Skip to content

Delegate primary script pathfinding and loading to PHP #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ Cargo.lock
!.yarn/versions

*.node
*.dylib

# Added by static-php-cli
crates/php/buildroot
Expand Down
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ compile_flags.txt
rustfmt.toml
pnpm-lock.yaml
*.node
*.dylib
!npm/**/*.node
!npm/**/*.dylib
__test__
renovate.json
57 changes: 39 additions & 18 deletions __test__/handler.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@ import test from 'ava'

import { Php, Request } from '../index.js'

import { MockRoot } from './util.mjs'

test('Support input/output streams', async (t) => {
const php = new Php({
argv: process.argv,
file: 'index.php',
code: `<?php
const mockroot = await MockRoot.from({
'index.php': `<?php
if (file_get_contents('php://input') == 'Hello, from Node.js!') {
echo 'Hello, from PHP!';
}
?>`
})
t.teardown(() => mockroot.clean())

const php = new Php({
argv: process.argv,
docroot: mockroot.path
})

const req = new Request({
method: 'GET',
url: 'http://example.com/test.php',
url: 'http://example.com/index.php',
body: Buffer.from('Hello, from Node.js!')
})

Expand All @@ -25,16 +31,21 @@ test('Support input/output streams', async (t) => {
})

test('Capture logs', async (t) => {
const php = new Php({
file: 'index.php',
code: `<?php
const mockroot = await MockRoot.from({
'index.php': `<?php
error_log('Hello, from error_log!');
?>`
})
t.teardown(() => mockroot.clean())

const php = new Php({
argv: process.argv,
docroot: mockroot.path
})

const req = new Request({
method: 'GET',
url: 'http://example.com/test.php'
url: 'http://example.com/index.php'
})

const res = await php.handleRequest(req)
Expand All @@ -43,16 +54,21 @@ test('Capture logs', async (t) => {
})

test('Capture exceptions', async (t) => {
const php = new Php({
file: 'index.php',
code: `<?php
const mockroot = await MockRoot.from({
'index.php': `<?php
throw new Exception('Hello, from PHP!');
?>`
})
t.teardown(() => mockroot.clean())

const php = new Php({
argv: process.argv,
docroot: mockroot.path
})

const req = new Request({
method: 'GET',
url: 'http://example.com/test.php'
url: 'http://example.com/index.php'
})

const res = await php.handleRequest(req)
Expand All @@ -62,19 +78,24 @@ test('Capture exceptions', async (t) => {
})

test('Support request and response headers', async (t) => {
const php = new Php({
file: 'index.php',
code: `<?php
const mockroot = await MockRoot.from({
'index.php': `<?php
$headers = apache_request_headers();
header("X-Test: Hello, from PHP!");
// TODO: Does PHP expect headers be returned to uppercase?
echo $headers["x-test"];
echo $headers["X-Test"];
?>`
})
t.teardown(() => mockroot.clean())

const php = new Php({
argv: process.argv,
docroot: mockroot.path
})

const req = new Request({
method: 'GET',
url: 'http://example.com/test.php',
url: 'http://example.com/index.php',
headers: {
'X-Test': ['Hello, from Node.js!']
}
Expand Down
10 changes: 5 additions & 5 deletions __test__/headers.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ test('includes iterator methods', (t) => {
const entries = Array.from(headers.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
t.deepEqual(entries, [
['accept', ['application/json']],
['content-type', ['application/json']]
['Accept', ['application/json']],
['Content-Type', ['application/json']]
])

const keys = Array.from(headers.keys()).sort()
t.deepEqual(keys, ['accept', 'content-type'])
t.deepEqual(keys, ['Accept', 'Content-Type'])

const values = Array.from(headers.values()).sort()
t.deepEqual(values, ['application/json', 'application/json'])
Expand All @@ -77,7 +77,7 @@ test('includes iterator methods', (t) => {
seen.push([name, values, map])
})
t.deepEqual(seen.sort((a, b) => a[0].localeCompare(b[0])), [
['accept', ['application/json'], headers],
['content-type', ['application/json'], headers]
['Accept', ['application/json'], headers],
['Content-Type', ['application/json'], headers]
])
})
62 changes: 62 additions & 0 deletions __test__/util.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { randomUUID } from 'node:crypto'
import { writeFile, mkdir, rmdir } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

const base = tmpdir()

export class MockRoot {
/**
* Creates a mock docroot using a nested object to represent the directory
* structure. A directory has a
*
* Example:
*
* ```js
* const dir = mockroot({
* 'hello.txt': 'Hello, World!',
* 'subdir': {
* 'subfile.txt': Buffer.from('hi')
* }
* })
* ```
*
* @param {*} files
*/
constructor(name = randomUUID()) {
this.path = join(base, name)
}

async writeFiles(files, base = this.path) {
await mkdir(base, { recursive: true })

for (let [name, contents] of Object.entries(files)) {
if (typeof contents === 'string') {
contents = Buffer.from(contents)
}

const path = join(base, name)
if (Buffer.isBuffer(contents)) {
await writeFile(path, contents)
} else {
await this.writeFiles(contents, path)
}
}
}

static async from(files) {
const mockroot = new MockRoot()
await mockroot.writeFiles(files)
return mockroot
}

/**
* Cleanup the mock docroot
*/
async clean() {
await rmdir(this.path, {
recursive: true,
force: true
})
}
}
19 changes: 11 additions & 8 deletions crates/lang_handler/src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ impl Headers {
where
K: AsRef<str>,
{
self.0.contains_key(key.as_ref().to_lowercase().as_str())
self
.0
.contains_key(key.as_ref() /*.to_lowercase().as_str()*/)
}

/// Returns the last single value associated with a header field.
Expand All @@ -77,7 +79,7 @@ impl Headers {
where
K: AsRef<str>,
{
match self.0.get(key.as_ref().to_lowercase().as_str()) {
match self.0.get(key.as_ref() /*.to_lowercase().as_str()*/) {
Some(Header::Single(value)) => Some(value.clone()),
Some(Header::Multiple(values)) => values.last().cloned(),
None => None,
Expand Down Expand Up @@ -106,7 +108,7 @@ impl Headers {
where
K: AsRef<str>,
{
match self.0.get(key.as_ref().to_lowercase().as_str()) {
match self.0.get(key.as_ref() /*.to_lowercase().as_str()*/) {
Some(Header::Single(value)) => vec![value.clone()],
Some(Header::Multiple(values)) => values.clone(),
None => Vec::new(),
Expand Down Expand Up @@ -162,9 +164,10 @@ impl Headers {
K: Into<String>,
V: Into<String>,
{
self
.0
.insert(key.into().to_lowercase(), Header::Single(value.into()));
self.0.insert(
key.into(), /*.to_lowercase()*/
Header::Single(value.into()),
);
}

/// Add a header with the given value without replacing existing ones.
Expand All @@ -187,7 +190,7 @@ impl Headers {
K: Into<String>,
V: Into<String>,
{
let key = key.into().to_lowercase();
let key = key.into()/*.to_lowercase()*/;
let value = value.into();

match self.0.entry(key) {
Expand Down Expand Up @@ -227,7 +230,7 @@ impl Headers {
where
K: AsRef<str>,
{
self.0.remove(key.as_ref().to_lowercase().as_str());
self.0.remove(key.as_ref() /*.to_lowercase().as_str()*/);
}

/// Clears all headers.
Expand Down
2 changes: 1 addition & 1 deletion crates/lang_handler/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ impl ResponseBuilder {
K: Into<String>,
V: Into<String>,
{
self.headers.set(key, value);
self.headers.add(key, value);
self
}

Expand Down
Loading
Loading