├── .editorconfig
├── .github
├── .kodiak.toml
├── ISSUE_TEMPLATE
│ ├── Bug.md
│ ├── Feature.md
│ └── Question.md
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── conf
└── build.ini
├── errors
└── now-dev-no-local-php.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── index.ts
├── launchers
│ ├── builtin.ts
│ ├── cgi.ts
│ ├── cli.ts
│ └── helpers.ts
├── types.d.ts
└── utils.ts
├── test
├── examples
│ ├── 00-php
│ │ ├── .gitignore
│ │ ├── api
│ │ │ ├── api
│ │ │ │ ├── index.php
│ │ │ │ └── users.php
│ │ │ ├── ext
│ │ │ │ ├── ds.php
│ │ │ │ ├── gd.php
│ │ │ │ ├── index.php
│ │ │ │ └── phalcon.php
│ │ │ ├── hello.php
│ │ │ ├── index.php
│ │ │ ├── ini
│ │ │ │ └── index.php
│ │ │ ├── libs.php
│ │ │ └── test.php
│ │ └── vercel.json
│ ├── 00-test
│ │ ├── .gitignore
│ │ ├── api
│ │ │ ├── hey.txt
│ │ │ ├── index.php
│ │ │ ├── php.ini
│ │ │ └── test.php
│ │ ├── src
│ │ │ └── foo.txt
│ │ └── vercel.json
│ ├── 01-cowsay
│ │ ├── index.php
│ │ ├── subdirectory
│ │ │ └── index.php
│ │ └── vercel.json
│ ├── 02-extensions
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 03-env-vars
│ │ ├── env
│ │ │ └── index.php
│ │ └── vercel.json
│ ├── 04-include-files
│ │ ├── excluded_file.php
│ │ ├── included_file.php
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 05-globals
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 06-setcookie
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 07-function
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 08-opcache
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 09-routes
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 10-composer-builds
│ │ ├── .gitignore
│ │ ├── .vercelignore
│ │ ├── composer.json
│ │ ├── composer.lock
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 11-composer-env
│ │ ├── .gitignore
│ │ ├── .vercelignore
│ │ ├── composer-test.json
│ │ ├── composer-test.lock
│ │ ├── index.php
│ │ └── vercel.json
│ ├── 12-composer
│ │ ├── .gitignore
│ │ ├── .vercelignore
│ │ ├── api
│ │ │ └── index.php
│ │ ├── composer.json
│ │ └── vercel.json
│ ├── 13-composer-scripts
│ │ ├── .gitignore
│ │ ├── .vercelignore
│ │ ├── api
│ │ │ └── index.php
│ │ ├── composer.json
│ │ └── vercel.json
│ ├── 14-folders
│ │ ├── .gitignore
│ │ ├── api
│ │ │ ├── index.php
│ │ │ └── users
│ │ │ │ ├── index.php
│ │ │ │ └── users.php
│ │ └── vercel.json
│ ├── 16-php-ini
│ │ ├── .gitignore
│ │ ├── api
│ │ │ ├── index.php
│ │ │ └── php.ini
│ │ └── vercel.json
│ ├── 17-zero
│ │ ├── .gitignore
│ │ ├── api
│ │ │ ├── index.php
│ │ │ └── test.html
│ │ ├── src
│ │ │ └── index.txt
│ │ └── vercel.json
│ ├── 18-exclude-files
│ │ ├── .gitignore
│ │ ├── .vercelignore
│ │ ├── api
│ │ │ └── index.php
│ │ ├── baz
│ │ │ └── index.html
│ │ ├── foo
│ │ │ └── index.txt
│ │ └── vercel.json
│ ├── 19-server-workers
│ │ ├── .gitignore
│ │ ├── api
│ │ │ └── index.php
│ │ └── vercel.json
│ └── 20-read-files
│ │ ├── .gitignore
│ │ ├── api
│ │ └── index.php
│ │ ├── src
│ │ └── users.json
│ │ └── vercel.json
└── spec
│ ├── index.dev.js
│ ├── index.js
│ ├── launchers
│ └── cgi.js
│ ├── path.js
│ └── url.js
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{html}]
12 | indent_style = tab
13 | indent_size = tab
14 | tab_width = 4
15 |
16 | [*.{js,ts,json,yml,yaml,md}]
17 | indent_style = space
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.github/.kodiak.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [merge]
4 | automerge_label = "automerge"
5 | blacklist_title_regex = "^WIP.*"
6 | blacklist_labels = ["WIP"]
7 | method = "rebase"
8 | delete_branch_on_merge = true
9 | notify_on_conflict = true
10 | optimistic_updates = false
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report 🐛
3 | about: Something is not working as expected!
4 | ---
5 |
6 | # Bug report
7 |
8 | - Version: x.y.z
9 | - URL: Yes (*.now.sh) / No
10 | - Repository: Yes / No
11 |
12 | ## Description
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request 🚀
3 | about: I would appreciate new feature or something!
4 | ---
5 |
6 | # Feature Request
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question ❓
3 | about: Ask about anything!
4 | ---
5 |
6 | # Question
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: '11:00'
8 | open-pull-requests-limit: 10
9 | - package-ecosystem: npm
10 | directory: "/packages/php"
11 | schedule:
12 | interval: daily
13 | time: '11:00'
14 | open-pull-requests-limit: 10
15 | - package-ecosystem: npm
16 | directory: "/packages/caddy"
17 | schedule:
18 | interval: daily
19 | time: '11:00'
20 | open-pull-requests-limit: 10
21 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | run:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v3
11 |
12 | - name: Setup Node
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version: '14.x'
16 |
17 | - name: Dependencies
18 | run: make install
19 |
20 | - name: Build
21 | run: make build
22 |
23 | - name: Tests
24 | run: make test
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node
2 | /node_modules
3 | /package-lock.json
4 |
5 | # App
6 | /dist
7 |
8 | # Vercel
9 | .vercel
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### [0.7.3] - 2024-10-19
4 |
5 | - Upgrade PHP 8.3 runtime (fixes for curl, opcache)
6 |
7 | ### [0.7.2] - 2024-09-30
8 |
9 | - Upgrade PHP 8.3 runtime
10 |
11 | ### [0.7.1] - 2024-04-16
12 |
13 | - Fix autodetect runtime
14 |
15 | ### [0.6.2] - 2024-04-16
16 |
17 | - Fix autodetect runtime
18 |
19 | ### [0.5.5] - 2024-04-16
20 |
21 | - Fix autodetect runtime
22 |
23 | ### [0.4.4] - 2024-04-16
24 |
25 | - Fix autodetect runtime
26 |
27 | ### [0.3.6] - 2024-04-16
28 |
29 | - Fix autodetect runtime
30 |
31 | ### [0.7.0] - 2024-02-22
32 |
33 | - PHP 8.3
34 | - Use `@libphp/amazon-linux-2-v83: latest`
35 |
36 | ### [0.6.1] - 2024-01-24
37 |
38 | - Update LD_LIBRARY_PATH
39 |
40 | ### [0.5.4] - 2024-01-24
41 |
42 | - Update LD_LIBRARY_PATH
43 |
44 | ### [0.4.3] - 2024-01-24
45 |
46 | - Update LD_LIBRARY_PATH
47 |
48 | ### [0.3.5] - 2024-01-24
49 |
50 | - Update LD_LIBRARY_PATH
51 |
52 | ### [0.6.0] - 2023-03-27
53 |
54 | - PHP 8.2
55 | - Use `@libphp/amazon-linux-2-v82: latest`
56 |
57 | ### [0.5.3] - 2023-03-27
58 |
59 | - Bump minimum node version from 14.x to 18.x
60 | - Upgrade dependencies
61 |
62 | ### [0.5.2] - 2022-08-10
63 |
64 | - Bump minimum node version from 12.x to 14.x
65 |
66 | ### [0.5.1] - 2022-05-05
67 |
68 | - Ignore .vercel folder during deployment
69 |
70 | ### [0.5.0] - 2022-04-09
71 |
72 | - PHP 8.1
73 | - Added extensions: geoip, zlib, zip
74 | - Removed extensions: psr
75 | - Use `@libphp/amazon-linux-2-v81: latest`
76 |
77 | ### [0.4.0] - 2021-01-02
78 |
79 | - PHP 8.0
80 | - Use `@libphp/amazon-linux-2-v80: latest`
81 |
82 | ### [0.3.2] - 2021-01-02
83 |
84 | - Typos
85 | - More hints in FAQ
86 | - Fix `excludeFiles` option
87 | - Install PHP extensions mongodb
88 | - Use `@libphp/amazon-linux-2-v74: latest`
89 |
90 | ### [0.3.1] - 2020-07-04
91 |
92 | - Install PHP extensions redis, msgpack, igbinary
93 | - Use `@libphp/amazon-linux-2-v74: latest`
94 |
95 | ### [0.3.0] - 2020-06-29
96 |
97 | - Allow to execute composer script called `vercel`
98 |
99 | ```json
100 | {
101 | "scripts": {
102 | "vercel": [
103 | "@php -v",
104 | "npm -v"
105 | ]
106 | }
107 | }
108 | ```
109 |
110 | - Drop support of `config['php.ini']` use `api/php.ini` file instead
111 | - Support excludeFiles (default `['node_modules/**', 'now.json', '.nowignore']`)
112 |
113 | ```json
114 | {
115 | "functions": {
116 | "api/**/*.php": {
117 | "runtime": "vercel-php@0.3.0",
118 | "excludeFiles": ["node_modules", "somedir", "foo/bar"],
119 | }
120 | }
121 | ```
122 |
123 | - Restructure test folder (merge fixtures + my examples)
124 |
125 | ### [0.2.0] - 2020-06-26
126 |
127 | - Allow to override `php.ini`
128 |
129 | ```sh
130 | project
131 | ├── api
132 | │ ├── index.php
133 | │ └── php.ini
134 | └── now.json
135 | ```
136 |
137 | - Extensive update of docs
138 | - Introduce FAQ questions
139 | - Move caddy package to [juicyfx/juicy](https://github.com/juicyfx/juicy)
140 | - Simplify repository structure
141 |
142 | ### [0.1.0] - 2020-06-20
143 |
144 | - Rename repository from now-php to **vercel-php**
145 | - Rename NPM package from now-php to **vercel-php**
146 | - Upgrade PHP to 7.4.7 and recompile PHP extensions
147 | - Improve readme
148 | - Separate PHP libs to solo repository [juicyfx/libphp](https://github.com/juicyfx/libphp) (bigger plans)
149 | - Use [php.vercel.app](https://php.vercel.app) domain for official showtime
150 | - Use [phpshow.vercel.app](https://phpshow.vercel.app) domain for runtime showcase
151 |
152 | ### [0.0.9] - 2020-03-28
153 |
154 | - Use PHP 7.4 for installing Composer dependencies
155 | - Upgrade PHP 7.4 and recompile PHP extensions
156 |
157 | ### [0.0.9] - 2020-01-16
158 |
159 | - Use PHP 7.3 for installing Composer dependencies
160 | - Separate [examples](https://github.com/juicyfx/vercel-examples) to solo repository
161 | - Extensions
162 | - Disabled ssh2
163 | - Added psr
164 | - Rebuild phalcon, swoole
165 |
166 | ### [0.0.8] - 2020-01-07
167 |
168 | - Runtime v3
169 | - Upgrade to PHP 7.4.x
170 | - Node 8.x reached EOL on AWS
171 | - Used Amazon Linux 2
172 | - CGI launcher inherits process.env [#38]
173 | - Drop Circle CI
174 | - Rebuild all PHP libs
175 |
176 | ### [0.0.7] - 2019-11-08
177 |
178 | - Rename builder to runtime
179 | - Runtime v3
180 |
181 | **Migration**
182 |
183 | ```json
184 | {
185 | "version": 2,
186 | "builds": [
187 | {
188 | "src": "index.php",
189 | "use": "now-php"
190 | }
191 | ]
192 | }
193 | ```
194 |
195 | ➡️
196 |
197 | ```json
198 | {
199 | "functions": {
200 | "api/*.php": {
201 | "runtime": "now-php@0.0.7"
202 | }
203 | },
204 | // Optionally provide routes
205 | "routes": [
206 | { "src": "/(.*)", "dest": "/api/index.php" }
207 | ]
208 | }
209 | ```
210 |
211 | ### [0.0.6] - 2019-11-07
212 |
213 | - Change builds to functions
214 |
215 | ### [0.0.5] - 2019-09-30
216 |
217 | - Added Lumen example
218 | - Bugfix deploying PHP files in folders under different names then index.php
219 |
220 | ### [0.0.4] - 2019-09-30
221 |
222 | - Implement intermediate caching (vendor, composer.lock, yarn.locak, package-lock.json, node_modules)
223 | - Rewrite PHP built-in server document root
224 |
225 | ### [0.0.3] - 2019-09-04
226 |
227 | - Bugfix passing query parameters and accessing $_GET
228 |
229 | ### [0.0.2] - 2019-08-23
230 |
231 | - Bump now-php@latest
232 |
233 | ### [0.0.1-canary.39] - 2019-08-23
234 |
235 | - Allow overriding php.ini
236 | - Bugfix resolving PHP bin
237 | - Bugfix deploying php files in subfolders
238 |
239 | ### [0.0.2-canary.2] - 2019-08-16
240 |
241 | - Compile PHP 7.3.8
242 |
243 | ### [0.0.1-canary.5] - 2019-08-16
244 |
245 | - First working copy of caddy server
246 |
247 | ### [0.0.1-canary.30] - 2019-08-16
248 |
249 | - New exported method `getPhpLibFiles`
250 | - Repair tests
251 |
252 | ### [0.0.1-canary.18] - 2019-08-02
253 |
254 | - Bump now-php@latest
255 |
256 | ### [0.0.1-canary.18] - 2019-08-02
257 |
258 | - Working on change response from string to Buffer
259 | - Updated homepage
260 |
261 | ### [0.0.1-canary.17] - 2019-08-02
262 |
263 | - Working on change response from string to Buffer
264 |
265 | ### [0.0.1-canary.15] - 2019-08-02
266 |
267 | - CGI: REQUEST_URI contains only path, not host + path
268 | - CGI: QUERY_STRING contains string without leading ?
269 |
270 | ### [0.0.1-canary.14] - 2019-07-29
271 |
272 | - Tests: more tests
273 |
274 | ### [0.0.1-canary.13] - 2019-07-29
275 |
276 | - Tests: take tests from official old builder
277 |
278 | ### [0.0.1-canary.12] - 2019-07-28
279 |
280 | - Rewritten to TypeScript
281 |
282 | ### [0.0.1-canary.11] - 2019-07-28
283 |
284 | - Working on `now-dev`
285 |
286 | ### [0.0.1-canary.8] - 2019-07-27
287 |
288 | - First working `now-php` builder
289 |
290 | ### [0.0.1-canary.7] - 2019-07-27
291 |
292 | - Working on `now` with `now-php`
293 |
294 | ### [0.0.1-canary.0] - 2019-07-27
295 |
296 | - History begins
297 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Juicy(fx)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: install build test publish canary
2 |
3 | install:
4 | npm install
5 |
6 | build:
7 | npm run build
8 |
9 | build-watch:
10 | npm run watch
11 |
12 | test:
13 | npm run test
14 |
15 | test-watch:
16 | npm run test-watch
17 |
18 | publish:
19 | rm -rf ./dist
20 | npm publish --access public --tag latest
21 |
22 | canary:
23 | rm -rf ./dist
24 | npm publish --access public --tag canary
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
PHP Runtime for Vercel
2 |
3 |
4 | Enjoyable & powerful 🐘 PHP Runtime (php.vercel.app) for Vercel platform.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 🏋️♀️ It works with these frameworks and tools. Discover more at examples.
24 |
25 |
26 | Made with ❤️ by @f3l1x (f3l1x.io) • 🐦 @xf3l1x
27 |
28 |
29 | -----
30 |
31 | ## 😎 Getting Started
32 |
33 | Let's picture you want to deploy your awesome microproject written in PHP and you don't know where. You have found [Vercel](https://vercel.com) it's awesome, but for static sites. Not anymore! I would like to introduce you your new best friend `vercel-php`, PHP runtime for Vercel platform.
34 |
35 | Most simple example project is this one, using following project structure.
36 |
37 | ```sh
38 | project
39 | ├── api
40 | │ └── index.php
41 | └── vercel.json
42 | ```
43 |
44 | First file `api/index.php` is entrypoint of our application. It should be placed in **api** folder, it's very standard location for Vercel.
45 |
46 | ```php
47 |
79 |
80 | ## 🤗 Features
81 |
82 | - **Architecture**: PHP development server (🚀 fast enough)
83 | - **PHP version**: 8.3 (https://example-php-8-3.vercel.app)
84 | - **Extensions**: apcu, bcmath, brotli, bz2, calendar, Core, ctype, curl, date, dom, ds, exif, fileinfo, filter, ftp, geoip, gettext, hash, iconv, igbinary, imap, intl, json, libxml, lua, mbstring, mongodb, msgpack, mysqli, mysqlnd, openssl, pcntl, pcre, PDO, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, phalcon, Phar, protobuf, readline, redis, Reflection, runkit7, session, SimpleXML, soap, sockets, sodium, SPL, sqlite3, standard, swoole, timecop, tokenizer, uuid, xml, xmlreader, xmlrpc, xmlwriter, xsl, Zend OPcache, zlib, zip
85 | - **Speed**: cold ~250ms / warm ~5ms
86 | - **Memory**: ~90mb
87 | - **Frameworks**: Nette, Symfony, Lumen, Slim, Phalcon
88 |
89 | > List of all installable extensions is on this page https://blog.remirepo.net/pages/PECL-extensions-RPM-status.
90 |
91 | ## 💯 Versions
92 |
93 | - `vercel-php@0.7.3` - Node autodetect / PHP 8.3.x (https://example-php-8-3.vercel.app)
94 | - `vercel-php@0.6.2` - Node autodetect / PHP 8.2.x (https://example-php-8-2.vercel.app)
95 | - `vercel-php@0.5.5` - Node autodetect / PHP 8.1.x (https://example-php-8-1.vercel.app)
96 | - `vercel-php@0.4.4` - Node autodetect / PHP 8.0.x (https://example-php-8-0.vercel.app)
97 | - `vercel-php@0.3.6` - Node autodetect / PHP 7.4.x (https://example-php-7-4.vercel.app)
98 |
99 | ## ⚙️ Usage
100 |
101 | Before you can start using this runtime, you should learn about Vercel and [how runtimes](https://vercel.com/docs/runtimes?query=runtime#official-runtimes) works. Take a look at blogpost about [`Serverless Functions`](https://vercel.com/blog/customizing-serverless-functions).
102 |
103 | You should define `functions` property in `vercel.json` and list PHP files directly or using wildcard (*).
104 | If you need to route everything to index, use `routes` property.
105 |
106 | ```json
107 | {
108 | "functions": {
109 | "api/*.php": {
110 | "runtime": "vercel-php@0.7.3"
111 | }
112 | },
113 | "routes": [
114 | { "src": "/(.*)", "dest": "/api/index.php" }
115 | ]
116 | }
117 | ```
118 |
119 | Do you have more questions (❓)? Let's move to [FAQ](#%EF%B8%8F-faq).
120 |
121 | ## 👨💻 `vercel dev`
122 |
123 | For running `vercel dev` properly, you need to have PHP installed on your computer, [learn more](errors/now-dev-no-local-php.md).
124 | But it's PHP and as you know PHP has built-in development server. It works out of box.
125 |
126 | ```
127 | php -S localhost:8000 api/index.php
128 | ```
129 |
130 | ## 👀 Demo
131 |
132 | - official - https://php.vercel.app/
133 | - phpinfo - https://phpshow.vercel.app/
134 | - extensions - https://phpshow.vercel.app/ext/
135 | - ini - https://phpshow.vercel.app/ini/
136 | - JSON API - https://phpshow.vercel.app/api/users.php
137 | - test - https://phpshow.vercel.app/test.php
138 |
139 | 
140 |
141 | ## 🎯Examples
142 |
143 | - [PHP - fast & simple](https://github.com/juicyfx/vercel-examples/tree/master/php/)
144 | - [Composer - install dependencies](https://github.com/juicyfx/vercel-examples/tree/master/php-composer/)
145 | - [Framework - Laravel](https://github.com/juicyfx/vercel-examples/blob/master/php-laravel)
146 | - [Framework - Lumen](https://github.com/juicyfx/vercel-examples/blob/master/php-lumen)
147 | - [Framework - Nette](https://github.com/juicyfx/vercel-examples/blob/master/php-nette-tracy)
148 | - [Framework - Phalcon](https://github.com/juicyfx/vercel-examples/blob/master/php-phalcon)
149 | - [Framework - Slim](https://github.com/juicyfx/vercel-examples/blob/master/php-slim)
150 | - [Framework - Symfony - Microservice](https://github.com/juicyfx/vercel-examples/blob/master/php-symfony-microservice)
151 |
152 | Browse [more examples](https://github.com/juicyfx/vercel-examples). 👀
153 |
154 | ## 📜 Resources
155 |
156 | - [2019/10/23 - Code Examples](https://github.com/trainit/2019-10-hubbr-zeit)
157 | - [2019/10/19 - ZEIT - Deploy Serverless Microservices Right Now](https://slides.com/f3l1x/2019-10-19-zeit-deploy-serverless-microservices-right-now-vol2)
158 | - [2019/08/23 - Code Examples](https://github.com/trainit/2019-08-serverless-zeit-now)
159 | - [2019/07/07 - Bleeding Edge PHP on ZEIT Now](https://dev.to/nx1/bleeding-edge-php-on-zeit-now-565g)
160 | - [2019/06/06 - Code Examples](https://github.com/trainit/2019-06-zeit-now)
161 | - [2019/06/05 - ZEIT - Deploy Serverless Microservices Right Now](https://slides.com/f3l1x/2019-06-05-zeit-deploy-serverless-microservices-right-now) ([VIDEO](https://www.youtube.com/watch?v=IwhEGNDx3aE))
162 |
163 | ## 🚧 Roadmap
164 |
165 | See [roadmap issue](https://github.com/juicyfx/vercel-php/issues/3). Help wanted.
166 |
167 | ## ⁉️ FAQ
168 |
169 |
170 | 1. How to use more then one endpoint (index.php)?
171 |
172 | ```sh
173 | project
174 | ├── api
175 | │ ├── index.php
176 | │ ├── users.php
177 | │ └── books.php
178 | └── vercel.json
179 | ```
180 |
181 | ```
182 | {
183 | "functions": {
184 | "api/*.php": {
185 | "runtime": "vercel-php@0.7.3"
186 | },
187 |
188 | // Can be list also directly
189 |
190 | "api/index.php": {
191 | "runtime": "vercel-php@0.7.3"
192 | },
193 | "api/users.php": {
194 | "runtime": "vercel-php@0.7.3"
195 | },
196 | "api/books.php": {
197 | "runtime": "vercel-php@0.7.3"
198 | }
199 | }
200 | }
201 | ```
202 |
203 |
204 |
205 |
206 | 2. How to route everything to index?
207 |
208 | ```json
209 | {
210 | "functions": {
211 | "api/index.php": {
212 | "runtime": "vercel-php@0.7.3"
213 | }
214 | },
215 | "routes": [
216 | { "src": "/(.*)", "dest": "/api/index.php" }
217 | ]
218 | }
219 | ```
220 |
221 |
222 |
223 |
224 | 3. How to update memory limit?
225 |
226 | Additional function properties are `memory`, `maxDuration`. Learn more about [functions](https://vercel.com/docs/configuration#project/functions).
227 |
228 | ```json
229 | {
230 | "functions": {
231 | "api/*.php": {
232 | "runtime": "vercel-php@0.7.3",
233 | "memory": 3008,
234 | "maxDuration": 60
235 | }
236 | }
237 | }
238 | ```
239 |
240 |
241 |
242 |
243 | 4. How to use it with Composer?
244 |
245 | Yes, [Composer](https://getcomposer.org/) is fully supported.
246 |
247 | ```sh
248 | project
249 | ├── api
250 | │ └── index.php
251 | ├── composer.json
252 | └── vercel.json
253 | ```
254 |
255 | ```json
256 | {
257 | "functions": {
258 | "api/*.php": {
259 | "runtime": "vercel-php@0.7.3"
260 | }
261 | }
262 | }
263 | ```
264 |
265 | ```json
266 | {
267 | "require": {
268 | "php": "^8.1",
269 | "tracy/tracy": "^2.0"
270 | }
271 | }
272 | ```
273 |
274 | It's also good thing to create `.vercelignore` file and put `/vendor` folder to this file. It will not upload
275 | `/vendor` folder to Vercel platform.
276 |
277 |
278 |
279 |
280 | 5. How to override php.ini / php configuration ?
281 |
282 | Yes, you can override php configuration. Take a look at [default configuration](https://phpshow.vercel.app/) at first.
283 | Create a new file `api/php.ini` and place there your configuration. Don't worry, this particulary file will be
284 | removed during building phase on Vercel.
285 |
286 | ```sh
287 | project
288 | ├── api
289 | │ ├── index.php
290 | │ └── php.ini
291 | └── vercel.json
292 | ```
293 |
294 | ```json
295 | {
296 | "functions": {
297 | "api/*.php": {
298 | "runtime": "vercel-php@0.7.3"
299 | }
300 | }
301 | }
302 | ```
303 |
304 | ```json
305 | # Disable some functions
306 | disable_functions = "exec, system"
307 |
308 | # Update memory limit
309 | memory_limit=1024M
310 | ```
311 |
312 |
313 |
314 |
315 | 6. How to exclude some files or folders ?
316 |
317 | Runtimes support excluding some files or folders, [take a look at doc](https://vercel.com/docs/configuration?query=excludeFiles#project/functions).
318 |
319 | ```json
320 | {
321 | "functions": {
322 | "api/**/*.php": {
323 | "runtime": "vercel-php@0.7.3",
324 | "excludeFiles": "{foo/**,bar/config/*.yaml}",
325 | }
326 | }
327 | ```
328 |
329 | If you want to exclude files before uploading them to Vercel, use `.vercelignore` file.
330 |
331 |
332 |
333 |
334 | 7. How to call composer script(s) ?
335 |
336 | Calling composer scripts during build phase on Vercel is supported via script named `vercel`. You can easilly call php, npm or node.
337 |
338 | ```json
339 | {
340 | "require": { ... },
341 | "require-dev": { ... },
342 | "scripts": {
343 | "vercel": [
344 | "@php -v",
345 | "npm -v"
346 | ]
347 | }
348 | }
349 | ```
350 |
351 | Files created during `composer run vercel` script can be used (require/include) in your PHP lambdas, but can't be accessed from browser (like assets). If you still want to access them, create fake `assets.php` lambda and require them. [Example of PHP satis](https://github.com/juicyfx/vercel-examples/tree/master/php-satis).
352 |
353 |
354 |
355 |
356 | 8. How to include some files of folders?
357 |
358 | If you are looking for [`config.includeFiles`](https://vercel.com/docs/configuration?query=includeFiles#project/functions) in runtime, unfortunately you can't include extra files.
359 | All files in root folder are uploaded to Vercel, use `.vercelignore` to exclude them before upload.
360 |
361 |
362 |
363 |
364 | 9. How to develop locally?
365 |
366 | I think the best way at this moment is use [PHP Development Server](https://www.php.net/manual/en/features.commandline.webserver.php).
367 |
368 | ```
369 | php -S localhost:8000 api/index.php
370 | ```
371 |
372 |
373 |
374 | ## 👨🏻💻CHANGELOG
375 |
376 | Show me [CHANGELOG](./CHANGELOG.md)
377 |
378 | ## 🧙Contribution
379 |
380 | 1. Clone this repository.
381 | - `git clone git@github.com:juicyfx/vercel-php.git`
382 | 2. Install NPM dependencies
383 | - `make install`
384 | 3. Make your changes
385 | 4. Run TypeScript compiler
386 | - `make build`
387 | 5. Run tests
388 | - `make test`
389 | 6. Create a PR
390 |
391 | ## 📝 License
392 |
393 | Copyright © 2019 [f3l1x](https://github.com/f3l1x).
394 | This project is [MIT](LICENSE) licensed.
395 |
--------------------------------------------------------------------------------
/conf/build.ini:
--------------------------------------------------------------------------------
1 | ; Override for build phase on Vercel
2 | extension_dir = "${PHP_INI_EXTENSION_DIR}"
3 |
--------------------------------------------------------------------------------
/errors/now-dev-no-local-php.md:
--------------------------------------------------------------------------------
1 | # It looks like you don't have PHP on your machine.
2 |
3 | **Why This Error Occurred**
4 |
5 | You ran `now dev` on a machine where PHP is not installed.
6 | For the time being, this runtime requires a local PHP installation to run the runtime locally.
7 |
8 | **Possible Ways to Fix It**
9 |
10 | 1. Install PHP to your computer
11 |
12 | **OSX**
13 |
14 | ```
15 | brew install php@7.4
16 | ```
17 |
18 | **Ubuntu**
19 |
20 | ```
21 | apt-get -y install apt-transport-https lsb-release ca-certificates
22 | wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
23 | sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
24 | apt-get update
25 | apt-get install php7.4-cli php7.4-cgi php7.4-json php7.4-curl php7.4-mbstring
26 | ```
27 |
28 | **Fedora**
29 |
30 | ```
31 | yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
32 | yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
33 | yum install yum-utils
34 | yum-config-manager --enable remi-php74
35 | yum update
36 | yum install php74-cli php74-cgi php74-json php74-curl php74-mbstring
37 | ```
38 |
39 | 2. Start PHP built-in Development Server
40 |
41 | ```sh
42 | php -S localhost:8000 api/index.php
43 | ```
44 |
45 | **Check that php is in the path**
46 |
47 | If you do have installed PHP but still get this error, check that PHP executable is added to the PATH environment variable.
48 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rootDir: ".",
3 | verbose: true,
4 | testEnvironment: "node",
5 | testMatch: [
6 | "**/test/spec/**/*.js",
7 | ],
8 | testPathIgnorePatterns: [
9 | "/errors/",
10 | "/dist/",
11 | "/node_modules/",
12 | ],
13 | testTimeout: 10000
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vercel-php",
3 | "description": "Vercel PHP runtime",
4 | "version": "0.7.3",
5 | "license": "MIT",
6 | "main": "./dist/index.js",
7 | "homepage": "https://github.com/juicyfx/vercel-php",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/juicyfx/vercel-php.git"
11 | },
12 | "keywords": [
13 | "vercel",
14 | "zeit",
15 | "now",
16 | "php",
17 | "builder",
18 | "runtime",
19 | "serverless",
20 | "deployment"
21 | ],
22 | "scripts": {
23 | "watch": "tsc --watch",
24 | "build": "tsc",
25 | "test": "jest --silent",
26 | "test-watch": "jest --watch",
27 | "prepublishOnly": "tsc"
28 | },
29 | "files": [
30 | "dist",
31 | "conf"
32 | ],
33 | "dependencies": {
34 | "@libphp/amazon-linux-2-v83": ">=0.0.7"
35 | },
36 | "devDependencies": {
37 | "@types/glob": "8.1.0",
38 | "@types/node": "18.19.18",
39 | "@vercel/build-utils": "7.11.0",
40 | "jest": "29.7.0",
41 | "typescript": "5.4.5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import {
3 | rename,
4 | shouldServe,
5 | glob,
6 | download,
7 | Lambda,
8 | BuildV3,
9 | PrepareCache,
10 | getNodeVersion
11 | } from '@vercel/build-utils';
12 | import {
13 | getPhpFiles,
14 | getLauncherFiles,
15 | runComposerInstall,
16 | runComposerScripts,
17 | readRuntimeFile,
18 | modifyPhpIni,
19 | } from './utils';
20 |
21 | const COMPOSER_FILE = process.env.COMPOSER || 'composer.json';
22 |
23 | // ###########################
24 | // EXPORTS
25 | // ###########################
26 |
27 | export const version = 3;
28 |
29 | export const build: BuildV3 = async ({
30 | files,
31 | entrypoint,
32 | workPath,
33 | config = {},
34 | meta = {},
35 | }) => {
36 | // Check if now dev mode is used
37 | if (meta.isDev) {
38 | console.log(`
39 | 🐘 vercel dev is not supported right now.
40 | Please use PHP built-in development server.
41 |
42 | php -S localhost:8000 api/index.php
43 | `);
44 | process.exit(255);
45 | }
46 |
47 | console.log('🐘 Downloading user files');
48 |
49 | // Collect user provided files
50 | const userFiles: RuntimeFiles = await download(files, workPath, meta);
51 |
52 | console.log('🐘 Downloading PHP runtime files');
53 |
54 | // Collect runtime files containing PHP bins and libs
55 | const runtimeFiles: RuntimeFiles = {
56 | // Append PHP files (bins + shared object)
57 | ...await getPhpFiles(),
58 |
59 | // Append launcher files (builtin server, common helpers)
60 | ...getLauncherFiles(),
61 | };
62 |
63 | // If composer.json is provided try to
64 | // - install deps
65 | // - run composer scripts
66 | if (userFiles[COMPOSER_FILE]) {
67 | // Install dependencies (vendor is collected bellow, see harvestedFiles)
68 | await runComposerInstall(workPath);
69 |
70 | // Run composer scripts (created files are collected bellow, , see harvestedFiles)
71 | await runComposerScripts(userFiles[COMPOSER_FILE], workPath);
72 | }
73 |
74 | // Append PHP directives into php.ini
75 | if (userFiles['api/php.ini']) {
76 | const phpini = await modifyPhpIni(userFiles, runtimeFiles);
77 | if (phpini) {
78 | runtimeFiles['php/php.ini'] = phpini;
79 | }
80 | }
81 |
82 | // Collect user files, files creating during build (composer vendor)
83 | // and other files and prefix them with "user" (/var/task/user folder).
84 | const harverstedFiles = rename(
85 | await glob('**', {
86 | cwd: workPath,
87 | ignore: [
88 | '.vercel/**',
89 | ...(config?.excludeFiles
90 | ? Array.isArray(config.excludeFiles)
91 | ? config.excludeFiles
92 | : [config.excludeFiles]
93 | : [
94 | 'node_modules/**',
95 | 'now.json',
96 | '.nowignore',
97 | 'vercel.json',
98 | '.vercelignore',
99 | ]),
100 | ],
101 | }),
102 | name => path.join('user', name)
103 | );
104 |
105 | // Show some debug notes during build
106 | if (process.env.NOW_PHP_DEBUG === '1') {
107 | console.log('🐘 Entrypoint:', entrypoint);
108 | console.log('🐘 Config:', config);
109 | console.log('🐘 Work path:', workPath);
110 | console.log('🐘 Meta:', meta);
111 | console.log('🐘 User files:', Object.keys(harverstedFiles));
112 | console.log('🐘 Runtime files:', Object.keys(runtimeFiles));
113 | console.log('🐘 PHP: php.ini', await readRuntimeFile(runtimeFiles['php/php.ini']));
114 | }
115 |
116 | console.log('🐘 Creating lambda');
117 | const nodeVersion = await getNodeVersion(workPath);
118 |
119 | const lambda = new Lambda({
120 | files: {
121 | // Located at /var/task/user
122 | ...harverstedFiles,
123 | // Located at /var/task/php (php bins + ini + modules)
124 | // Located at /var/task/lib (shared libs)
125 | ...runtimeFiles
126 | },
127 | handler: 'launcher.launcher',
128 | runtime: nodeVersion.runtime,
129 | environment: {
130 | NOW_ENTRYPOINT: entrypoint,
131 | NOW_PHP_DEV: meta.isDev ? '1' : '0'
132 | },
133 | });
134 |
135 | return { output: lambda };
136 | };
137 |
138 | export const prepareCache: PrepareCache = async ({ workPath }) => {
139 | return {
140 | // Composer
141 | ...(await glob('vendor/**', workPath)),
142 | ...(await glob('composer.lock', workPath)),
143 | // NPM
144 | ...(await glob('node_modules/**', workPath)),
145 | ...(await glob('package-lock.json', workPath)),
146 | ...(await glob('yarn.lock', workPath)),
147 | };
148 | };
149 |
150 | export { shouldServe };
151 |
--------------------------------------------------------------------------------
/src/launchers/builtin.ts:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import { spawn, ChildProcess, SpawnOptions } from 'child_process';
3 | import net from 'net';
4 | import {
5 | getPhpDir,
6 | getUserDir,
7 | normalizeEvent,
8 | transformFromAwsRequest,
9 | transformToAwsResponse,
10 | isDev
11 | } from './helpers';
12 | import { join as pathJoin } from 'path';
13 |
14 | let server: ChildProcess;
15 |
16 | async function startServer(entrypoint: string): Promise {
17 | // Resolve document root and router
18 | const router = entrypoint;
19 | const docroot = pathJoin(getUserDir(), process.env.VERCEL_PHP_DOCROOT ?? '');
20 |
21 | console.log(`🐘 Spawning: PHP Built-In Server at ${docroot} (document root) and ${router} (router)`);
22 |
23 | // php spawn options
24 | const options: SpawnOptions = {
25 | stdio: ['pipe', 'pipe', 'pipe'],
26 | env: {
27 | ...process.env,
28 | LD_LIBRARY_PATH: `/var/task/lib:${process.env.LD_LIBRARY_PATH}`
29 | }
30 | };
31 |
32 | // now vs now-dev
33 | if (!isDev()) {
34 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`;
35 | options.cwd = getPhpDir();
36 | } else {
37 | options.cwd = getUserDir();
38 | }
39 |
40 | // We need to start PHP built-in server with following setup:
41 | // php -c php.ini -S ip:port -t /var/task/user /var/task/user/foo/bar.php
42 | //
43 | // Path to document root lambda task folder with user prefix, because we move all
44 | // user files to this folder.
45 | //
46 | // Path to router is absolute path, because CWD is different.
47 | //
48 | server = spawn(
49 | 'php',
50 | ['-c', 'php.ini', '-S', '0.0.0.0:3000', '-t', docroot, router],
51 | options,
52 | );
53 |
54 | server.stdout?.on('data', data => {
55 | console.log(`🐘STDOUT: ${data.toString()}`);
56 | });
57 |
58 | server.stderr?.on('data', data => {
59 | console.error(`🐘STDERR: ${data.toString()}`);
60 | });
61 |
62 | server.on('close', function (code, signal) {
63 | console.log(`🐘 PHP Built-In Server process closed code ${code} and signal ${signal}`);
64 | });
65 |
66 | server.on('error', function (err) {
67 | console.error(`🐘 PHP Built-In Server process errored ${err}`);
68 | });
69 |
70 | await whenPortOpens(3000, 500);
71 |
72 | process.on('exit', () => {
73 | server.kill();
74 | })
75 |
76 | return server;
77 | }
78 |
79 | async function query({ entrypoint, uri, path, headers, method, body }: PhpInput): Promise {
80 | if (!server) {
81 | await startServer(entrypoint);
82 | }
83 |
84 | return new Promise(resolve => {
85 | const options = {
86 | hostname: 'localhost',
87 | port: 3000,
88 | path,
89 | method,
90 | headers,
91 | };
92 |
93 | console.log(`🐘 Accessing ${uri}`);
94 | console.log(`🐘 Querying ${path}`);
95 |
96 | const req = http.request(options, (res) => {
97 | const chunks: Uint8Array[] = [];
98 |
99 | res.on('data', (data) => {
100 | chunks.push(data);
101 | });
102 | res.on('end', () => {
103 | resolve({
104 | statusCode: res.statusCode || 200,
105 | headers: res.headers,
106 | body: Buffer.concat(chunks)
107 | });
108 | });
109 | });
110 |
111 | req.on('error', (error) => {
112 | console.error('🐘 PHP Built-In Server HTTP errored', error);
113 | resolve({
114 | body: Buffer.from(`PHP Built-In Server HTTP error: ${error}`),
115 | headers: {},
116 | statusCode: 500
117 | });
118 | });
119 |
120 | if (body) {
121 | req.write(body);
122 | }
123 |
124 | req.end();
125 | });
126 | }
127 |
128 | function whenPortOpensCallback(port: number, attempts: number, cb: (error?: string) => void) {
129 | const client = net.connect(port, '127.0.0.1');
130 | client.on('error', (error: string) => {
131 | if (!attempts) return cb(error);
132 | setTimeout(() => {
133 | whenPortOpensCallback(port, attempts - 1, cb);
134 | }, 10);
135 | });
136 | client.on('connect', () => {
137 | client.destroy();
138 | cb();
139 | });
140 | }
141 |
142 | function whenPortOpens(port: number, attempts: number): Promise {
143 | return new Promise((resolve, reject) => {
144 | whenPortOpensCallback(port, attempts, (error?: string) => {
145 | if (error) {
146 | reject(error);
147 | } else {
148 | resolve();
149 | }
150 | });
151 | });
152 | }
153 |
154 | async function launcher(event: Event): Promise {
155 | const awsRequest = normalizeEvent(event);
156 | const input = await transformFromAwsRequest(awsRequest);
157 | const output = await query(input);
158 | return transformToAwsResponse(output);
159 | }
160 |
161 | exports.launcher = launcher;
162 |
163 | // (async function () {
164 | // const response = await launcher({
165 | // Action: "test",
166 | // httpMethod: "GET",
167 | // body: "",
168 | // path: "/",
169 | // host: "https://vercel.com",
170 | // headers: {
171 | // 'HOST': 'vercel.com'
172 | // },
173 | // encoding: null,
174 | // });
175 |
176 | // console.log(response);
177 | // })();
178 |
--------------------------------------------------------------------------------
/src/launchers/cgi.ts:
--------------------------------------------------------------------------------
1 | import { spawn, SpawnOptions } from 'child_process';
2 | import { parse as urlParse } from 'url';
3 | import {
4 | getPhpDir,
5 | getUserDir,
6 | normalizeEvent,
7 | transformFromAwsRequest,
8 | transformToAwsResponse,
9 | isDev
10 | } from './helpers';
11 |
12 | function createCGIReq({ entrypoint, path, host, method, headers }: CgiInput): CgiRequest {
13 | const { query } = urlParse(path);
14 |
15 | const env: Env = {
16 | ...process.env,
17 | SERVER_ROOT: getUserDir(),
18 | DOCUMENT_ROOT: getUserDir(),
19 | SERVER_NAME: host,
20 | SERVER_PORT: 443,
21 | HTTPS: "On",
22 | REDIRECT_STATUS: 200,
23 | SCRIPT_NAME: entrypoint,
24 | REQUEST_URI: path,
25 | SCRIPT_FILENAME: entrypoint,
26 | PATH_TRANSLATED: entrypoint,
27 | REQUEST_METHOD: method,
28 | QUERY_STRING: query || '',
29 | GATEWAY_INTERFACE: "CGI/1.1",
30 | SERVER_PROTOCOL: "HTTP/1.1",
31 | PATH: process.env.PATH,
32 | SERVER_SOFTWARE: "Vercel PHP",
33 | LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH
34 | };
35 |
36 | if (headers["content-length"]) {
37 | env.CONTENT_LENGTH = headers["content-length"];
38 | }
39 |
40 | if (headers["content-type"]) {
41 | env.CONTENT_TYPE = headers["content-type"];
42 | }
43 |
44 | if (headers["x-real-ip"]) {
45 | env.REMOTE_ADDR = headers["x-real-ip"];
46 | }
47 |
48 | // expose request headers
49 | Object.keys(headers).forEach(function (header) {
50 | var name = "HTTP_" + header.toUpperCase().replace(/-/g, "_");
51 | env[name] = headers[header];
52 | });
53 |
54 | return {
55 | env
56 | }
57 | }
58 |
59 | function parseCGIResponse(response: Buffer) {
60 | const headersPos = response.indexOf("\r\n\r\n");
61 | if (headersPos === -1) {
62 | return {
63 | headers: {},
64 | body: response,
65 | statusCode: 200
66 | }
67 | }
68 |
69 | let statusCode = 200;
70 | const rawHeaders = response.slice(0, headersPos).toString();
71 | const rawBody = response.slice(headersPos + 4);
72 |
73 | const headers = parseCGIHeaders(rawHeaders);
74 |
75 | if (headers['status']) {
76 | statusCode = parseInt(headers['status']) || 200;
77 | }
78 |
79 | return {
80 | headers,
81 | body: rawBody,
82 | statusCode
83 | }
84 | }
85 |
86 | function parseCGIHeaders(headers: string): CgiHeaders {
87 | if (!headers) return {}
88 |
89 | const result: CgiHeaders = {}
90 |
91 | for (let header of headers.split("\n")) {
92 | const index = header.indexOf(':');
93 | const key = header.slice(0, index).trim().toLowerCase();
94 | const value = header.slice(index + 1).trim();
95 |
96 | // Be careful about header duplication
97 | result[key] = value;
98 | }
99 |
100 | return result
101 | }
102 |
103 | function query({ entrypoint, path, host, headers, method, body }: PhpInput): Promise {
104 | console.log(`🐘 Spawning: PHP CGI ${entrypoint}`);
105 |
106 | // Transform lambda request to CGI variables
107 | const { env } = createCGIReq({ entrypoint, path, host, headers, method })
108 |
109 | // php-cgi spawn options
110 | const options: SpawnOptions = {
111 | stdio: ['pipe', 'pipe', 'pipe'],
112 | env: env
113 | };
114 |
115 | // now vs now-dev
116 | if (!isDev()) {
117 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`;
118 | options.cwd = getPhpDir();
119 | } else {
120 | options.cwd = getUserDir();
121 | }
122 |
123 | return new Promise((resolve) => {
124 | const chunks: Uint8Array[] = [];
125 |
126 | const php = spawn(
127 | 'php-cgi',
128 | [entrypoint],
129 | options,
130 | );
131 |
132 | // Validate pipes [stdin]
133 | if (!php.stdin) {
134 | console.error(`🐘 Fatal error. PHP CGI child process has no stdin.`);
135 | process.exit(253);
136 | }
137 |
138 | // Validate pipes [stdout]
139 | if (!php.stdout) {
140 | console.error(`🐘 Fatal error. PHP CGI child process has no stdout.`);
141 | process.exit(254);
142 | }
143 |
144 | // Validate pipes [stderr]
145 | if (!php.stderr) {
146 | console.error(`🐘 Fatal error. PHP CGI child process has no stderr.`);
147 | process.exit(255);
148 | }
149 |
150 | // Output
151 | php.stdout.on('data', data => {
152 | chunks.push(data);
153 | });
154 |
155 | // Logging
156 | php.stderr.on('data', data => {
157 | console.error(`🐘 PHP CGI stderr`, data.toString());
158 | });
159 |
160 | // PHP script execution end
161 | php.on('close', (code, signal) => {
162 | if (code !== 0) {
163 | console.log(`🐘 PHP CGI process closed code ${code} and signal ${signal}`);
164 | }
165 |
166 | const { headers, body, statusCode } = parseCGIResponse(Buffer.concat(chunks));
167 |
168 | resolve({
169 | body,
170 | headers,
171 | statusCode
172 | });
173 | });
174 |
175 | php.on('error', err => {
176 | console.error('🐘 PHP CGI errored', err);
177 | resolve({
178 | body: Buffer.from(`🐘 PHP CGI process errored ${err}`),
179 | headers: {},
180 | statusCode: 500
181 | });
182 | });
183 |
184 | // Writes the body into the PHP stdin
185 | php.stdin.write(body || '');
186 | php.stdin.end();
187 | })
188 | }
189 |
190 | async function launcher(event: Event): Promise {
191 | const awsRequest = normalizeEvent(event);
192 | const input = await transformFromAwsRequest(awsRequest);
193 | const output = await query(input);
194 | return transformToAwsResponse(output);
195 | }
196 |
197 | exports.createCGIReq = createCGIReq;
198 | exports.launcher = launcher;
199 |
200 | // (async function () {
201 | // const response = await launcher({
202 | // Action: "test",
203 | // httpMethod: "GET",
204 | // body: "",
205 | // path: "/",
206 | // host: "https://vercel.com",
207 | // headers: {
208 | // 'HOST': 'vercel.com'
209 | // },
210 | // encoding: null,
211 | // });
212 |
213 | // console.log(response);
214 | // })();
215 |
--------------------------------------------------------------------------------
/src/launchers/cli.ts:
--------------------------------------------------------------------------------
1 | import { spawn, SpawnOptions } from 'child_process';
2 | import {
3 | getPhpDir,
4 | normalizeEvent,
5 | transformFromAwsRequest,
6 | transformToAwsResponse,
7 | isDev,
8 | getUserDir
9 | } from './helpers';
10 |
11 | function query({ entrypoint, body }: PhpInput): Promise {
12 | console.log(`🐘 Spawning: PHP CLI ${entrypoint}`);
13 |
14 | // php spawn options
15 | const options: SpawnOptions = {
16 | stdio: ['pipe', 'pipe', 'pipe'],
17 | env: process.env
18 | };
19 |
20 | // now vs now-dev
21 | if (!isDev()) {
22 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`;
23 | options.cwd = getPhpDir();
24 | } else {
25 | options.cwd = getUserDir();
26 | }
27 |
28 | return new Promise((resolve) => {
29 | const chunks: Uint8Array[] = [];
30 |
31 | const php = spawn(
32 | 'php',
33 | ['-c', 'php.ini', entrypoint],
34 | options,
35 | );
36 |
37 | // Validate pipes [stdin]
38 | if (!php.stdin) {
39 | console.error(`🐘 Fatal error. PHP CLI child process has no stdin.`);
40 | process.exit(253);
41 | }
42 |
43 | // Validate pipes [stdout]
44 | if (!php.stdout) {
45 | console.error(`🐘 Fatal error. PHP CLI child process has no stdout.`);
46 | process.exit(254);
47 | }
48 |
49 | // Validate pipes [stderr]
50 | if (!php.stderr) {
51 | console.error(`🐘 Fatal error. PHP CLI child process has no stderr.`);
52 | process.exit(255);
53 | }
54 |
55 | // Output
56 | php.stdout.on('data', data => {
57 | chunks.push(data);
58 | });
59 |
60 | // Logging
61 | php.stderr.on('data', data => {
62 | console.error(`🐘 PHP CLI stderr`, data.toString());
63 | });
64 |
65 | // PHP script execution end
66 | php.on('close', (code, signal) => {
67 | if (code !== 0) {
68 | console.log(`🐘 PHP CLI process closed code ${code} and signal ${signal}`);
69 | }
70 |
71 | resolve({
72 | statusCode: 200,
73 | headers: {},
74 | body: Buffer.concat(chunks)
75 | });
76 | });
77 |
78 | php.on('error', err => {
79 | console.error('🐘 PHP CLI errored', err);
80 | resolve({
81 | body: Buffer.from(`🐘 PHP CLI process errored ${err}`),
82 | headers: {},
83 | statusCode: 500
84 | });
85 | });
86 |
87 | // Writes the body into the PHP stdin
88 | php.stdin.write(body || '');
89 | php.stdin.end();
90 | })
91 | }
92 |
93 | async function launcher(event: Event): Promise {
94 | const awsRequest = normalizeEvent(event);
95 | const input = await transformFromAwsRequest(awsRequest);
96 | const output = await query(input);
97 | return transformToAwsResponse(output);
98 | }
99 |
100 | exports.launcher = launcher;
101 |
102 | // (async function () {
103 | // const response = await launcher({
104 | // Action: "test",
105 | // httpMethod: "GET",
106 | // body: "",
107 | // path: "/",
108 | // host: "https://vercel.com",
109 | // headers: {
110 | // 'HOST': 'vercel.com'
111 | // },
112 | // encoding: null,
113 | // });
114 |
115 | // console.log(response);
116 | // })();
117 |
--------------------------------------------------------------------------------
/src/launchers/helpers.ts:
--------------------------------------------------------------------------------
1 | import { join as pathJoin } from 'path';
2 |
3 | export const getUserDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'user');
4 | export const getPhpDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'php');
5 | export const isDev = (): boolean => process.env.NOW_PHP_DEV === '1';
6 |
7 | export function normalizeEvent(event: Event): AwsRequest {
8 | if (event.Action === 'Invoke') {
9 | const invokeEvent = JSON.parse(event.body);
10 |
11 | const {
12 | method, path, host, headers = {}, encoding,
13 | }: InvokedEvent = invokeEvent;
14 |
15 | let { body } = invokeEvent;
16 |
17 | if (body) {
18 | if (encoding === 'base64') {
19 | body = Buffer.from(body, encoding);
20 | } else if (encoding === undefined) {
21 | body = Buffer.from(body);
22 | } else {
23 | throw new Error(`Unsupported encoding: ${encoding}`);
24 | }
25 | }
26 |
27 | return {
28 | method,
29 | path,
30 | host,
31 | headers,
32 | body,
33 | };
34 | }
35 |
36 | const {
37 | httpMethod: method, path, host, headers = {}, body,
38 | } = event;
39 |
40 | return {
41 | method,
42 | path,
43 | host,
44 | headers,
45 | body,
46 | };
47 | }
48 |
49 | export async function transformFromAwsRequest({
50 | method, path, host, headers, body,
51 | }: AwsRequest): Promise {
52 | if (!process.env.NOW_ENTRYPOINT) {
53 | console.error('Missing ENV NOW_ENTRYPOINT');
54 | }
55 |
56 | const entrypoint = pathJoin(
57 | getUserDir(),
58 | process.env.NOW_ENTRYPOINT || 'index.php',
59 | );
60 |
61 | const uri = host + path;
62 |
63 | return { entrypoint, uri, path, host, method, headers, body };
64 | }
65 |
66 | export function transformToAwsResponse({ statusCode, headers, body }: PhpOutput): AwsResponse {
67 | return { statusCode, headers, body: body.toString('base64'), encoding: 'base64' };
68 | }
69 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | type Headers = { [k: string]: string | string[] | undefined };
2 |
3 | interface UserFiles {
4 | [filePath: string]: import('@vercel/build-utils').File;
5 | }
6 |
7 | interface RuntimeFiles {
8 | [filePath: string]: import('@vercel/build-utils').File;
9 | }
10 |
11 | interface IncludedFiles {
12 | [filePath: string]: import('@vercel/build-utils').File;
13 | }
14 |
15 | interface MetaOptions {
16 | meta: import('@vercel/build-utils').Meta;
17 | }
18 |
19 | interface AwsRequest {
20 | method: string,
21 | path: string,
22 | host: string,
23 | headers: Headers,
24 | body: string,
25 | }
26 |
27 | interface AwsResponse {
28 | statusCode: number,
29 | headers: Headers,
30 | body: string,
31 | encoding?: string
32 | }
33 |
34 | interface Event {
35 | Action: string,
36 | body: string,
37 | httpMethod: string,
38 | path: string,
39 | host: string,
40 | headers: Headers,
41 | encoding: string | undefined | null,
42 | }
43 |
44 | interface InvokedEvent {
45 | method: string,
46 | path: string,
47 | host: string,
48 | headers: Headers,
49 | encoding: string | undefined | null,
50 | }
51 |
52 | interface CgiInput {
53 | entrypoint: string,
54 | path: string,
55 | host: string,
56 | method: string,
57 | headers: Headers,
58 | }
59 |
60 | interface PhpInput {
61 | entrypoint: string,
62 | path: string,
63 | uri: string,
64 | host: string,
65 | method: string,
66 | headers: Headers,
67 | body: string,
68 | }
69 |
70 | interface PhpOutput {
71 | statusCode: number,
72 | headers: Headers,
73 | body: Buffer,
74 | }
75 |
76 | interface CgiHeaders {
77 | [k: string]: string,
78 | }
79 |
80 | interface CgiRequest {
81 | env: Env,
82 | }
83 |
84 | interface Env {
85 | [k: string]: any,
86 | }
87 |
88 | interface PhpIni {
89 | [k: string]: any,
90 | }
91 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { spawn, SpawnOptions } from 'child_process';
3 | import { File, FileFsRef, FileBlob } from '@vercel/build-utils';
4 | import * as libphp from "@libphp/amazon-linux-2-v83";
5 |
6 | const PHP_PKG = path.dirname(require.resolve('@libphp/amazon-linux-2-v83/package.json'));
7 | const PHP_BIN_DIR = path.join(PHP_PKG, "native/php");
8 | const PHP_MODULES_DIR = path.join(PHP_BIN_DIR, "modules");
9 | const PHP_LIB_DIR = path.join(PHP_PKG, "native/lib");
10 | const COMPOSER_BIN = path.join(PHP_BIN_DIR, "composer");
11 |
12 | export async function getPhpFiles(): Promise {
13 | const files = await libphp.getFiles();
14 |
15 | // Drop CGI + FPM from libphp, it's not needed for our case
16 | delete files['php/php-cgi'];
17 | delete files['php/php-fpm'];
18 | delete files['php/php-fpm.ini'];
19 |
20 | const runtimeFiles: RuntimeFiles = {};
21 |
22 | // Map from @libphp to Vercel's File objects
23 | for (const [filename, filepath] of Object.entries(files)) {
24 | runtimeFiles[filename] = new FileFsRef({
25 | fsPath: filepath
26 | })
27 | }
28 |
29 | // Set some bins executable
30 | (runtimeFiles['php/php'] as FileFsRef).mode = 33261; // 0755;
31 | (runtimeFiles['php/composer'] as FileFsRef).mode = 33261; // 0755;
32 |
33 | return runtimeFiles;
34 | }
35 |
36 | export function getLauncherFiles(): RuntimeFiles {
37 | const files: RuntimeFiles = {
38 | 'helpers.js': new FileFsRef({
39 | fsPath: path.join(__dirname, 'launchers/helpers.js'),
40 | })
41 | }
42 |
43 | files['launcher.js'] = new FileFsRef({
44 | fsPath: path.join(__dirname, 'launchers/builtin.js'),
45 | });
46 |
47 | return files;
48 | }
49 |
50 | export async function modifyPhpIni(userFiles: UserFiles, runtimeFiles: RuntimeFiles): Promise {
51 | // Validate user files contains php.ini
52 | if (!userFiles['api/php.ini']) return;
53 |
54 | // Validate runtime contains php.ini
55 | if (!runtimeFiles['php/php.ini']) return;
56 |
57 | const phpiniBlob = await FileBlob.fromStream({
58 | stream: runtimeFiles['php/php.ini'].toStream(),
59 | });
60 |
61 | const userPhpiniBlob = await FileBlob.fromStream({
62 | stream: userFiles['api/php.ini'].toStream(),
63 | });
64 |
65 | return new FileBlob({
66 | data: phpiniBlob.data.toString()
67 | .concat("; [User]\n")
68 | .concat(userPhpiniBlob.data.toString())
69 | });
70 | }
71 |
72 | export async function runComposerInstall(workPath: string): Promise {
73 | console.log('🐘 Installing Composer dependencies [START]');
74 |
75 | // @todo PHP_COMPOSER_INSTALL env
76 | await runPhp(
77 | [
78 | COMPOSER_BIN,
79 | 'install',
80 | '--profile',
81 | '--no-dev',
82 | '--no-interaction',
83 | '--no-scripts',
84 | '--ignore-platform-reqs',
85 | '--no-progress'
86 | ],
87 | {
88 | stdio: 'inherit',
89 | cwd: workPath
90 | }
91 | );
92 |
93 | console.log('🐘 Installing Composer dependencies [DONE]');
94 | }
95 |
96 | export async function runComposerScripts(composerFile: File, workPath: string): Promise {
97 | let composer;
98 |
99 | try {
100 | composer = JSON.parse(await readRuntimeFile(composerFile));
101 | } catch (e) {
102 | console.error('🐘 Composer file is not valid JSON');
103 | console.error(e);
104 | return;
105 | }
106 |
107 | if (composer?.scripts?.vercel) {
108 | console.log('🐘 Running composer scripts [START]');
109 |
110 | await runPhp(
111 | [COMPOSER_BIN, 'run', 'vercel'],
112 | {
113 | stdio: 'inherit',
114 | cwd: workPath
115 | }
116 | );
117 |
118 | console.log('🐘 Running composer scripts [DONE]');
119 | }
120 | }
121 |
122 | export async function ensureLocalPhp(): Promise {
123 | try {
124 | await spawnAsync('which', ['php', 'php-cgi'], { stdio: 'pipe' });
125 | return true;
126 | } catch (e) {
127 | return false;
128 | }
129 | }
130 |
131 | export async function readRuntimeFile(file: File): Promise {
132 | const blob = await FileBlob.fromStream({
133 | stream: file.toStream(),
134 | });
135 |
136 | return blob.data.toString();
137 | }
138 |
139 | // *****************************************************************************
140 | // PRIVATE API *****************************************************************
141 | // *****************************************************************************
142 |
143 | async function runPhp(args: ReadonlyArray, opts: SpawnOptions = {}) {
144 | try {
145 | await spawnAsync('php', args,
146 | {
147 | ...opts,
148 | env: {
149 | ...process.env,
150 | ...(opts.env || {}),
151 | COMPOSER_HOME: '/tmp',
152 | PATH: `${PHP_BIN_DIR}:${process.env.PATH}`,
153 | PHP_INI_EXTENSION_DIR: PHP_MODULES_DIR,
154 | PHP_INI_SCAN_DIR: `:${path.resolve(__dirname, '../conf')}`,
155 | LD_LIBRARY_PATH: `${PHP_LIB_DIR}:/usr/lib64:/lib64:${process.env.LD_LIBRARY_PATH}`
156 | }
157 | }
158 | );
159 | } catch (e) {
160 | console.error(e);
161 | process.exit(1);
162 | }
163 | }
164 |
165 | function spawnAsync(command: string, args: ReadonlyArray, opts: SpawnOptions = {}): Promise {
166 | return new Promise((resolve, reject) => {
167 | const child = spawn(command, args, {
168 | stdio: "ignore",
169 | ...opts
170 | });
171 |
172 | child.on('error', reject);
173 | child.on('exit', (code, signal) => {
174 | if (code === 0) {
175 | resolve();
176 | } else {
177 | reject(new Error(`Exited with ${code || signal}`));
178 | }
179 | });
180 | })
181 | }
182 |
--------------------------------------------------------------------------------
/test/examples/00-php/.gitignore:
--------------------------------------------------------------------------------
1 | # Vercel
2 | .vercel
3 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/api/index.php:
--------------------------------------------------------------------------------
1 | 1, 'name' => 'f3l1x'],
5 | ['id' => 2, 'name' => 'chemix'],
6 | ['id' => 3, 'name' => 'dg'],
7 | ['id' => 4, 'name' => 'milo'],
8 | ['id' => 5, 'name' => 'matej21'],
9 | ['id' => 6, 'name' => 'merxes'],
10 | ];
11 |
12 | header('Content-Type: application/json');
13 |
14 | echo json_encode($data);
15 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/ext/ds.php:
--------------------------------------------------------------------------------
1 | 1, "b" => 2, "c" => 3]);
3 | $map->apply(function($key, $value) { return $value * 2; });
4 |
5 | print_r($map);
6 | ?>
--------------------------------------------------------------------------------
/test/examples/00-php/api/ext/gd.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/ext/index.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/ext/phalcon.php:
--------------------------------------------------------------------------------
1 | get(
8 | "/",
9 | function () {
10 | echo "Welcome!
";
11 | }
12 | );
13 |
14 | $app->get(
15 | "/say/hello/{name}",
16 | function ($name) use ($app) {
17 | echo "Hello! $name
";
18 | echo "Your IP Address is ", $app->request->getClientAddress();
19 | }
20 | );
21 |
22 | $app->post(
23 | "/store/something",
24 | function () use ($app) {
25 | $name = $app->request->getPost("name");
26 | echo "Hello! $name
";
27 | }
28 | );
29 |
30 | $app->notFound(
31 | function () use ($app) {
32 | $app->response->setStatusCode(404, "Not Found");
33 | $app->response->sendHeaders();
34 | echo "This is crazy, but this page was not found!";
35 | }
36 | );
37 |
38 | $app->handle();
39 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/hello.php:
--------------------------------------------------------------------------------
1 | php.ini
2 |
3 |
4 |
5 |
6 | option |
7 | global_value |
8 | local_value |
9 | access |
10 |
11 |
12 |
13 | $group) {
15 | echo "";
16 | echo sprintf('%s | ', $name);
17 | echo sprintf('%s | ', $group['global_value']);
18 | echo sprintf('%s | ', $group['local_value']);
19 | echo sprintf('%s | ', $group['access']);
20 | echo "
";
21 | }
22 | ?>
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/libs.php:
--------------------------------------------------------------------------------
1 | ";
9 | } else {
10 | $files = scandir($path);
11 | echo "Scan folder: $path
";
12 | array_map(function($file) {
13 | echo "- $file
";
14 | }, $files);
15 | }
16 | echo "
";
17 | }
18 |
--------------------------------------------------------------------------------
/test/examples/00-php/api/test.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/test/examples/02-extensions/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [{ "src": "index.php", "use": "vercel-php@0.7.3" }]
4 | }
5 |
--------------------------------------------------------------------------------
/test/examples/03-env-vars/env/index.php:
--------------------------------------------------------------------------------
1 | =5.3.0"
25 | },
26 | "type": "library",
27 | "extra": {
28 | "branch-alias": {
29 | "dev-master": "1.0.x-dev"
30 | }
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Psr\\Log\\": "Psr/Log/"
35 | }
36 | },
37 | "notification-url": "https://packagist.org/downloads/",
38 | "license": [
39 | "MIT"
40 | ],
41 | "authors": [
42 | {
43 | "name": "PHP-FIG",
44 | "homepage": "http://www.php-fig.org/"
45 | }
46 | ],
47 | "description": "Common interface for logging libraries",
48 | "homepage": "https://github.com/php-fig/log",
49 | "keywords": [
50 | "log",
51 | "psr",
52 | "psr-3"
53 | ],
54 | "time": "2018-11-20T15:27:04+00:00"
55 | }
56 | ],
57 | "packages-dev": [],
58 | "aliases": [],
59 | "minimum-stability": "stable",
60 | "stability-flags": [],
61 | "prefer-stable": false,
62 | "prefer-lowest": false,
63 | "platform": [],
64 | "platform-dev": []
65 | }
66 |
--------------------------------------------------------------------------------
/test/examples/10-composer-builds/index.php:
--------------------------------------------------------------------------------
1 | =5.3.0"
25 | },
26 | "type": "library",
27 | "extra": {
28 | "branch-alias": {
29 | "dev-master": "1.0.x-dev"
30 | }
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Psr\\Log\\": "Psr/Log/"
35 | }
36 | },
37 | "notification-url": "https://packagist.org/downloads/",
38 | "license": [
39 | "MIT"
40 | ],
41 | "authors": [
42 | {
43 | "name": "PHP-FIG",
44 | "homepage": "http://www.php-fig.org/"
45 | }
46 | ],
47 | "description": "Common interface for logging libraries",
48 | "homepage": "https://github.com/php-fig/log",
49 | "keywords": [
50 | "log",
51 | "psr",
52 | "psr-3"
53 | ],
54 | "time": "2018-11-20T15:27:04+00:00"
55 | }
56 | ],
57 | "packages-dev": [],
58 | "aliases": [],
59 | "minimum-stability": "stable",
60 | "stability-flags": [],
61 | "prefer-stable": false,
62 | "prefer-lowest": false,
63 | "platform": [],
64 | "platform-dev": []
65 | }
66 |
--------------------------------------------------------------------------------
/test/examples/11-composer-env/index.php:
--------------------------------------------------------------------------------
1 | {
4 | const mockLog = console.log = jest.fn();
5 |
6 | jest.spyOn(process, 'exit').mockImplementation((code) => {
7 | expect(code).toBe(255);
8 | expect(mockLog).toHaveBeenCalledTimes(1);
9 | });
10 |
11 | await builder.build({
12 | files: [],
13 | entrypoint: 'test.php',
14 | workPath: __dirname,
15 | config: {},
16 | meta: { isDev: true },
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/test/spec/index.js:
--------------------------------------------------------------------------------
1 | const builder = require('./../../dist/index');
2 |
3 | test('creates simple lambda', async () => {
4 | await builder.build({
5 | files: [],
6 | entrypoint: 'test.php',
7 | workPath: __dirname,
8 | config: {},
9 | meta: {},
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/test/spec/launchers/cgi.js:
--------------------------------------------------------------------------------
1 | const cgi = require('./../../../dist/launchers/cgi');
2 |
3 | test('create CGI request', () => {
4 | const request = {
5 | entrypoint: "index.php",
6 | path: "/index.php",
7 | host: "https://vercel.com",
8 | method: "GET",
9 | headers: {}
10 | };
11 | process.env.CUSTOM_VALUE = "custom-value";
12 | const { env } = cgi.createCGIReq(request);
13 |
14 | expect(env).toHaveProperty("SERVER_ROOT", "/user");
15 | expect(env).toHaveProperty("DOCUMENT_ROOT", "/user");
16 | expect(env).toHaveProperty("SERVER_NAME", request.host);
17 | expect(env).toHaveProperty("SERVER_PORT", 443);
18 | expect(env).toHaveProperty("HTTPS", 'On');
19 | expect(env).toHaveProperty("REDIRECT_STATUS", 200);
20 | expect(env).toHaveProperty("SCRIPT_NAME", request.entrypoint);
21 | expect(env).toHaveProperty("REQUEST_URI", request.path);
22 | expect(env).toHaveProperty("SCRIPT_FILENAME", request.entrypoint);
23 | expect(env).toHaveProperty("PATH_TRANSLATED", request.entrypoint);
24 | expect(env).toHaveProperty("REQUEST_METHOD", request.method);
25 | expect(env).toHaveProperty("QUERY_STRING", '');
26 | expect(env).toHaveProperty("GATEWAY_INTERFACE", 'CGI/1.1');
27 | expect(env).toHaveProperty("SERVER_PROTOCOL", 'HTTP/1.1');
28 | expect(env).toHaveProperty("SERVER_SOFTWARE", 'Vercel PHP');
29 | expect(env).toHaveProperty("PATH", process.env.PATH);
30 | expect(env).toHaveProperty("LD_LIBRARY_PATH", process.env.LD_LIBRARY_PATH);
31 | expect(env).toHaveProperty("CUSTOM_VALUE", process.env.CUSTOM_VALUE);
32 | });
33 |
--------------------------------------------------------------------------------
/test/spec/path.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | test('relative path', () => {
4 | const rootdir = '/var/task/user';
5 | const request = '/var/task/user/api/users.php';
6 | const file = path.relative(rootdir, request);
7 |
8 | expect(file).toBe('api/users.php');
9 | });
10 |
--------------------------------------------------------------------------------
/test/spec/url.js:
--------------------------------------------------------------------------------
1 | const url = require('url');
2 |
3 | test('url.parse search & query are string', () => {
4 | const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo');
5 | expect(search).toBe('?foo=bar&foo2=baz');
6 | expect(query).toBe('foo=bar&foo2=baz');
7 | });
8 |
9 | test('url.parse search string, query object', () => {
10 | const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo', true);
11 | expect(search).toBe('?foo=bar&foo2=baz');
12 | expect(query).toMatchObject({ foo: 'bar', 'foo2': 'baz' });
13 | });
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "esModuleInterop": true,
5 | "resolveJsonModule": true,
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true,
8 | "lib": [
9 | "ES2019"
10 | ],
11 | "target": "ES2019",
12 | "module": "CommonJS",
13 | "outDir": "dist",
14 | "sourceMap": false,
15 | "declaration": true,
16 | "noImplicitAny": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "forceConsistentCasingInFileNames": true,
20 | "skipLibCheck": true,
21 | "typeRoots": [
22 | "./node_modules/@types"
23 | ]
24 | },
25 | "include": [
26 | "src/**/*.ts"
27 | ],
28 | "exclude": [
29 | "errors",
30 | "dist",
31 | "node_modules",
32 | "test"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------