Skip to content
Draft
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
144 changes: 144 additions & 0 deletions packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,150 @@ describe('RawBytesFetch', () => {
);
expect(decodedRequestBody).toEqual(encodedBodyBytes);
});

it('parseHttpRequest should handle a path and query string', async () => {
const requestBytes = `GET /core/version-check/1.7/?channel=beta HTTP/1.1\r\nHost: playground.internal\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'playground.internal',
'http'
);
expect(request.url).toEqual(
'http://playground.internal/core/version-check/1.7/?channel=beta'
);
});

it('parseHttpRequest should handle a simple path without query string', async () => {
const requestBytes = `GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual('http://example.com/api/users');
});

it('parseHttpRequest should handle root path', async () => {
const requestBytes = `GET / HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'https'
);
expect(request.url).toEqual('https://example.com/');
});

it('parseHttpRequest should handle URL-encoded characters in path', async () => {
const requestBytes = `GET /search/hello%20world HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual('http://example.com/search/hello%20world');
});

it('parseHttpRequest should handle URL-encoded characters in query string', async () => {
const requestBytes = `GET /search?q=hello+world&filter=a%26b HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual(
'http://example.com/search?q=hello+world&filter=a%26b'
);
});

it('parseHttpRequest should handle empty query parameter values', async () => {
const requestBytes = `GET /api?key1=&key2=value2 HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual('http://example.com/api?key1=&key2=value2');
});

it('parseHttpRequest should handle path with hash fragment', async () => {
// Note: Hash fragments are typically not sent in HTTP requests,
// but if they are, the URL constructor should handle them
const requestBytes = `GET /page#section HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual('http://example.com/page#section');
});

it('parseHttpRequest should handle path with query and hash', async () => {
const requestBytes = `GET /page?param=value#section HTTP/1.1\r\nHost: example.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'example.com',
'http'
);
expect(request.url).toEqual(
'http://example.com/page?param=value#section'
);
});

it('parseHttpRequest should preserve Host header over default host', async () => {
const requestBytes = `GET /api HTTP/1.1\r\nHost: custom.host.com\r\n\r\n`;
const request = await RawBytesFetch.parseHttpRequest(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(requestBytes));
controller.close();
},
}),
'default.host.com', // Different from Host header
'https'
);
// Should use the Host header, not the default host parameter
expect(request.url).toEqual('https://custom.host.com/api');
});
});

type MakeRequestOptions = {
Expand Down
1 change: 0 additions & 1 deletion packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,6 @@ export class RawBytesFetch {
*/
const hostname = parsedHeaders.headers.get('Host') ?? host;
const url = new URL(parsedHeaders.path, protocol + '://' + hostname);
url.pathname = parsedHeaders.path;

return new Request(url.toString(), {
method: parsedHeaders.method,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { RecommendedPHPVersion } from '@wp-playground/common';
import type { SupportedPHPVersion } from '@php-wasm/universal';
import { BlueprintReflection } from './reflection';
import type { Blueprint, RuntimeConfiguration } from './types';
import { compileBlueprintV1 } from './v1/compile';
import type { BlueprintV1 } from './v1/types';

/**
* BlueprintOverrides type - matches the type from @wp-playground/client
* but defined here to avoid circular dependencies.
*/
export interface BlueprintOverrides {
blueprintOverrides?: {
wordpressVersion?: string;
phpVersion?: string;
additionalSteps?: any[];
};
applicationOptions?: {
landingPage?: string;
login?: boolean;
networkAccess?: boolean;
};
}

export async function resolveRuntimeConfiguration(
blueprint: Blueprint
blueprint: Blueprint,
overrides?: BlueprintOverrides
): Promise<RuntimeConfiguration> {
const reflection = await BlueprintReflection.create(blueprint);
if (reflection.getVersion() === 1) {
Expand All @@ -30,12 +49,42 @@ export async function resolveRuntimeConfiguration(
constants: {},
};
} else {
// @TODO: actually compute the runtime configuration based on the resolved Blueprint v2
// For Blueprint v2, compute runtime configuration from the blueprint and overrides
const declaration = reflection.getDeclaration() as any;

// Determine WordPress version (priority: override > blueprint > default)
const wpVersion =
overrides?.blueprintOverrides?.wordpressVersion ||
declaration.wordpressVersion ||
'latest';

// Determine PHP version (priority: override > blueprint > default)
let phpVersion: SupportedPHPVersion = RecommendedPHPVersion;
if (overrides?.blueprintOverrides?.phpVersion) {
phpVersion = overrides.blueprintOverrides
.phpVersion as SupportedPHPVersion;
} else if (declaration.phpVersion) {
// Handle both string and object forms of phpVersion
if (typeof declaration.phpVersion === 'string') {
phpVersion = declaration.phpVersion as SupportedPHPVersion;
} else if (declaration.phpVersion.recommended) {
phpVersion = declaration.phpVersion
.recommended as SupportedPHPVersion;
}
}

// Determine networking (priority: override > blueprint > default)
const networking =
overrides?.applicationOptions?.networkAccess ??
declaration.applicationOptions?.['wordpress-playground']
?.networkAccess ??
true;

return {
phpVersion: RecommendedPHPVersion,
wpVersion: 'latest',
phpVersion,
wpVersion,
intl: false,
networking: true,
networking,
constants: {},
extraLibraries: [],
};
Expand Down
34 changes: 30 additions & 4 deletions packages/playground/blueprints/src/lib/v2/run-blueprint-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export async function runBlueprintV2(
}
}
cliArgs.push('--site-path=/wordpress');
cliArgs.push('--allow=read-local-fs');

/**
* Divergence from blueprints.phar – the default database engine is
Expand Down Expand Up @@ -148,13 +149,29 @@ function playground_on_blueprint_resolved($blueprint) {
$additional_blueprint_steps = json_decode(${phpVar(
JSON.stringify(options.blueprintOverrides?.additionalSteps || [])
)}, true);

// TODO: detect v1 step format vs v2 stepformat
if(count($additional_blueprint_steps) > 0) {
$blueprint['additionalStepsAfterExecution'] = array_merge(
$blueprint['additionalStepsAfterExecution'] ?? [],
$additional_blueprint_steps
// Additional steps from URL overrides are in v1 format
// We need to transpile them to v2 format before merging
$transpiler = new WordPress\\Blueprints\\Versions\\Version1\\V1ToV2Transpiler(
new WordPress\\Blueprints\\Logger\\NoopLogger()
);
$temp_v1_blueprint = [
'steps' => $additional_blueprint_steps
];
$upgraded_blueprint = $transpiler->upgrade($temp_v1_blueprint);

// Extract the transpiled steps from the upgraded blueprint
$transpiled_steps = $upgraded_blueprint['additionalStepsAfterExecution'] ?? [];

if(count($transpiled_steps) > 0) {
$blueprint['additionalStepsAfterExecution'] = array_merge(
$blueprint['additionalStepsAfterExecution'] ?? [],
$transpiled_steps
);
}
}

$wp_version_override = json_decode(${phpVar(
JSON.stringify(options.blueprintOverrides?.wordpressVersion || null)
)}, true);
Expand Down Expand Up @@ -223,5 +240,14 @@ require( "/tmp/blueprints.phar" );

streamedResponse.finished.finally(unbindMessageListener);

// TODO: Report these errors in the web implementation
streamedResponse.stderr.pipeTo(
new WritableStream({
write(chunk) {
console.log('stderr', new TextDecoder().decode(chunk));
},
})
);

return streamedResponse;
}
Loading
Loading