Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const build: BuildV3 = async ({
]),
],
}),
name => path.join('user', name)
name => path.posix.join('user', name.replace(/\\/g, '/'))
);

// Show some debug notes during build
Expand Down
8 changes: 7 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ const PHP_LIB_DIR = path.join(PHP_PKG, "native/lib");
const COMPOSER_BIN = path.join(PHP_BIN_DIR, "composer");

export async function getPhpFiles(): Promise<RuntimeFiles> {
const files = await libphp.getFiles();
// Bundle keys are POSIX paths; physical file paths retain host separators.
const files = Object.fromEntries(
Object.entries(await libphp.getFiles()).map(([filename, filepath]) => [
filename.replace(/\\/g, '/'),
filepath,
])
);

// Drop CGI + FPM from libphp, it's not needed for our case
delete files['php/php-cgi'];
Expand Down
22 changes: 18 additions & 4 deletions test/spec/launchers/cgi.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
const path = require('path');
const cgi = require('./../../../dist/launchers/cgi');

test('create CGI request', () => {
afterEach(() => {
jest.restoreAllMocks();
});

test.each([
['default root', undefined],
['configured root', path.resolve('/var/task')],
])('create CGI request with %s', (_name, taskRoot) => {
const request = {
entrypoint: "index.php",
path: "/index.php",
host: "https://vercel.com",
method: "GET",
headers: {}
};
process.env.CUSTOM_VALUE = "custom-value";
jest.replaceProperty(process, 'env', { ...process.env, CUSTOM_VALUE: 'custom-value' });
if (taskRoot === undefined) {
delete process.env.LAMBDA_TASK_ROOT;
} else {
process.env.LAMBDA_TASK_ROOT = taskRoot;
}
const { env } = cgi.createCGIReq(request);
const userDir = path.join(taskRoot || '/', 'user');

expect(env).toHaveProperty("SERVER_ROOT", "/user");
expect(env).toHaveProperty("DOCUMENT_ROOT", "/user");
expect(env).toHaveProperty("SERVER_ROOT", userDir);
expect(env).toHaveProperty("DOCUMENT_ROOT", userDir);
expect(env).toHaveProperty("SERVER_NAME", request.host);
expect(env).toHaveProperty("SERVER_PORT", 443);
expect(env).toHaveProperty("HTTPS", 'On');
Expand Down
2 changes: 1 addition & 1 deletion test/spec/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ test('relative path', () => {
const request = '/var/task/user/api/users.php';
const file = path.relative(rootdir, request);

expect(file).toBe('api/users.php');
expect(file).toBe(path.join('api', 'users.php'));
});
129 changes: 129 additions & 0 deletions test/spec/windows-bundle-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const path = require('path');

jest.mock('@libphp/almalinux-9-v85', () => {
const actual = jest.requireActual('@libphp/almalinux-9-v85');
return { ...actual, getFiles: jest.fn(actual.getFiles) };
});

jest.mock('@vercel/build-utils', () => {
const actual = jest.requireActual('@vercel/build-utils');
return { ...actual, glob: jest.fn(actual.glob) };
});

const libphp = require('@libphp/almalinux-9-v85');
const { FileFsRef, FileBlob, glob } = require('@vercel/build-utils');
const { getPhpFiles, modifyPhpIni, readRuntimeFile } = require('../../dist/utils');
const { build } = require('../../dist/index');
const actualLibphp = jest.requireActual('@libphp/almalinux-9-v85');
const excluded = ['php/php-cgi', 'php/php-fpm', 'php/php-fpm.ini'];
const fixturePath = path.resolve(__dirname, '../examples/01-cowsay');
const userIni = 'memory_limit = 192M\n';

function logicalKey(name, separators, index) {
const posixName = name.replace(/\\/g, '/');
return separators === 'backslash' || (separators === 'mixed' && index % 2 === 0)
? posixName.replace(/\//g, '\\')
: posixName;
}

async function verifyRuntimeFiles(inputFiles) {
const runtimeFiles = await getPhpFiles();
const normalized = Object.fromEntries(
Object.entries(inputFiles).map(([name, fsPath]) => [name.replace(/\\/g, '/'), fsPath])
);

expect(Object.keys(runtimeFiles).sort()).toEqual(
Object.keys(normalized).filter(name => !excluded.includes(name)).sort()
);
for (const name of excluded) {
expect(runtimeFiles).not.toHaveProperty(name);
}
for (const [name, file] of Object.entries(runtimeFiles)) {
expect(name).not.toContain('\\');
expect(file).toBeInstanceOf(FileFsRef);
expect(file.fsPath).toBe(normalized[name]);
}
expect(runtimeFiles['php/php'].mode).toBe(33261);
expect(runtimeFiles['php/composer'].mode).toBe(33261);
expect(runtimeFiles['php/php.ini']).toBeInstanceOf(FileFsRef);

const merged = await modifyPhpIni(
{ 'api/php.ini': new FileBlob({ data: userIni }) },
runtimeFiles
);
expect(merged).toBeInstanceOf(FileBlob);
expect(merged.data.toString()).toBe(
`${await readRuntimeFile(runtimeFiles['php/php.ini'])}; [User]\n${userIni}`
);
}

afterEach(() => {
jest.clearAllMocks();
});

test('collects the installed runtime with native keys and unchanged physical paths', async () => {
await verifyRuntimeFiles(await actualLibphp.getFiles());
});

test.each(['slash', 'backslash', 'mixed'])(
'normalizes %s runtime keys before exclusions and preserves the source map',
async separators => {
const installedFiles = await actualLibphp.getFiles();
const inputFiles = Object.fromEntries(
Object.entries(installedFiles).map(([name, fsPath], index) => [
logicalKey(name, separators, index), fsPath,
])
);
const original = { ...inputFiles };
libphp.getFiles.mockResolvedValueOnce(inputFiles);

await verifyRuntimeFiles(inputFiles);
expect(inputFiles).toEqual(original);
}
);

test('builds a real fixture with POSIX bundle keys', async () => {
const { output } = await build({
files: {},
entrypoint: 'index.php',
workPath: fixturePath,
config: {},
meta: {},
});

expect(output.files['user/index.php']).toBeInstanceOf(FileFsRef);
expect(output.files['php/php.ini']).toBeInstanceOf(FileFsRef);
expect(Object.keys(output.files).every(name => !name.includes('\\'))).toBe(true);
for (const name of excluded) {
expect(output.files).not.toHaveProperty(name);
}
});

test.each(['slash', 'backslash', 'mixed'])(
'prefixes %s harvested keys with user/ without changing file references',
async separators => {
const names = ['api/index.php', 'vendor/package/file.php'];
const refs = names.map(() => new FileFsRef({
fsPath: path.join(fixturePath, 'index.php'),
}));
glob.mockResolvedValueOnce(Object.fromEntries(
names.map((name, index) => [logicalKey(name, separators, index), refs[index]])
));

const { output } = await build({
files: {},
entrypoint: 'api/index.php',
workPath: fixturePath,
config: {},
meta: {},
});

expect(Object.keys(output.files).filter(name => name.startsWith('user/')).sort())
.toEqual(names.map(name => `user/${name}`).sort());
names.forEach((name, index) => {
expect(output.files[`user/${name}`]).toBe(refs[index]);
expect(output.files[`user/${name}`].fsPath).toBe(path.join(fixturePath, 'index.php'));
});
expect(Object.keys(output.files).every(name => !name.includes('\\'))).toBe(true);
}
);