Skip to content
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

Explorations: Stream API #851

Closed
wants to merge 74 commits into from
Closed
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4b7dacf
Explore unzipping files using DecompressionStream instead of passing …
adamziel Dec 9, 2023
b776c7b
Remove custom importing logic, use Blueprint steps instead
adamziel Dec 9, 2023
17f4bb4
Scanning remote zip file
adamziel Dec 11, 2023
3e21b3c
Working zip scanning through repeated fetch() calls!
adamziel Dec 11, 2023
c2abb74
Fast chunked download
adamziel Dec 11, 2023
035b3f1
Simplify
adamziel Dec 11, 2023
92c4ad0
Explore stream concatenation
adamziel Dec 11, 2023
019ff4e
Use makeReadableByteStream
adamziel Dec 11, 2023
1646ef6
Move more towards streams
adamziel Dec 12, 2023
27bfce3
Read central directory headers in one go
adamziel Dec 12, 2023
a382076
API Cleanup
adamziel Dec 12, 2023
5fa7b47
Experiment: listZipFiles returns a stream
adamziel Dec 12, 2023
bbac923
Getting somewhere with the API shape
adamziel Dec 12, 2023
9eee842
Embrace streams for partitioning and fetching the partitioned data
adamziel Dec 12, 2023
c05a0bf
Precompute lastByteAt
adamziel Dec 12, 2023
53826ee
Save one fetch() request by reusing the bytes downloaded to find the …
adamziel Dec 12, 2023
afb62d3
Scan for the signature from the end to the start
adamziel Dec 12, 2023
de89933
Tidied up API
adamziel Dec 12, 2023
2f8838e
Implement iterateFromUrl
adamziel Dec 12, 2023
ca16410
Clean up types
adamziel Dec 12, 2023
2e4f891
Download 110KB when scanning the directory index
adamziel Dec 12, 2023
ff59b06
Clean up
adamziel Dec 12, 2023
37aab97
Don't do ranges if the no predicate is specified
adamziel Dec 13, 2023
4045568
Check for ranges query support
adamziel Dec 13, 2023
42850a4
Confirm ranges work and reuse the response stream if they don't
adamziel Dec 13, 2023
8edcc06
Cleanup the code
adamziel Dec 13, 2023
6980d21
Split into separate files
adamziel Dec 13, 2023
b049d62
Update 01-index.md
adamziel Dec 13, 2023
f4fd9cd
Support compressing files
adamziel Dec 14, 2023
27f1c24
Use Uint8Arrays to represent strings internally
adamziel Dec 14, 2023
2035e3c
Code style
adamziel Dec 14, 2023
b8c9e1c
Move common functions to utils folder
adamziel Dec 14, 2023
6ddc393
Remove PHP zipping logic
adamziel Dec 14, 2023
52885ae
Remove readAllBytes helper
adamziel Dec 14, 2023
234e161
Harmonize the API
adamziel Dec 14, 2023
51bfe5b
Refactor the changeset function to handle two iterables
adamziel Dec 14, 2023
a358100
Replace FileEntry with File
adamziel Dec 14, 2023
8a7274c
Remove the text() property from ZipFileEntry
adamziel Dec 14, 2023
9c9d257
Document helper methods
adamziel Dec 14, 2023
6f0e3bf
Move streaming function to their own package (@wp-playground/stream-c…
adamziel Dec 15, 2023
eb42147
Cleanup the API
adamziel Dec 15, 2023
4c11253
Add comments and inline documentation
adamziel Dec 15, 2023
726fc68
Clean up the API
adamziel Dec 15, 2023
052d0fb
Merge branch 'trunk' into explore-decompression-streams-and-iterators
adamziel Dec 15, 2023
0386863
Add basic unit tests
adamziel Dec 15, 2023
96cf31f
Fix a typo
adamziel Dec 15, 2023
8bdea1f
Remove the dependency check eslint rule
adamziel Dec 16, 2023
93ac815
Adjust stream-compression/package.json
adamziel Dec 16, 2023
b2e4fbd
Polyfill CustomEvent and Blob methods for Node.js
adamziel Dec 16, 2023
dad4b8d
Polyfill deflate-raw compression method using the gzip compression me…
adamziel Dec 17, 2023
a8ed4c0
Lint
adamziel Dec 17, 2023
2bf3e1f
Fix Blueprints export
adamziel Dec 17, 2023
481e44a
Restore the correct repo name
adamziel Dec 17, 2023
dc37c01
Fix dev artifacts in Blob polyfills
adamziel Dec 17, 2023
00e3de9
Adjust unit tests
adamziel Dec 17, 2023
21c025c
ADd vitest setups
adamziel Dec 17, 2023
6b8aee3
Restore trunk's dependencies
Dec 17, 2023
345eced
Fix the error where vitest cannot find php-wasm/node-polyfills
Dec 17, 2023
6b3924c
General cleanup
Dec 17, 2023
e7dc0af
Restore BYOB stream polyifll
Dec 17, 2023
0046aef
Fix isByobSupported() check
Dec 17, 2023
ca9e681
Fix ESMCJS check in GitHub CI
Dec 17, 2023
46c36eb
Handle invalid content-length values
Dec 17, 2023
2d31a1f
Remove esmcjs test from wp-playground/wordpress project.json
Dec 17, 2023
b35ffb3
Fix Blueprints unit tests
Dec 17, 2023
0719f67
Simplify Blueprints tests
Dec 17, 2023
a4df29f
Preserve the BYOB stream property when monitoring the download progress
adamziel Dec 18, 2023
919a17b
Adjust error messages in unit tests
adamziel Dec 18, 2023
3548789
Fix unit tests
adamziel Dec 18, 2023
0cd54f2
Wait a minute before failing the Gutenberg plugin installed E2E test
adamziel Dec 18, 2023
0d50cc7
Merge branch 'trunk' into explore-decompression-streams-and-iterators
adamziel Jan 8, 2024
3e2d4cd
Move the stream-compression logic to php-wasm
adamziel Jan 8, 2024
41cffb5
Clean up package-lock.json and remove unnecessary file-polyfill.ts
adamziel Jan 8, 2024
4d0a0e2
Merge branch 'trunk' into explore-decompression-streams-and-iterators
adamziel Jan 8, 2024
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
6 changes: 6 additions & 0 deletions packages/docs/site/docs/01-start-here/01-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ slug: /

# WordPress Playground

:::info **Looking for the official Playground website?**

WordPress Playground website was moved to [wp.org/playground](wp.org/playground). The site you're at right now is now a home for the documentation.

:::

👋 Hi! Welcome to WordPress Playground documentation. Playground is an online tool to experiment and learn about WordPress – learn more in the [overview section](./02-overview.md).

The documentation consists of two major sections:
Expand Down
3 changes: 3 additions & 0 deletions packages/php-wasm/universal/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export type {
SpawnHandler,
} from './universal-php';

export { iterateFiles, readAllBytes, writeToPath } from './iterate-files';
export type { FileEntry, IterateFilesOptions } from './iterate-files';

export { UnhandledRejectionsTarget } from './wasm-error-reporting';

export { PHPResponse } from './php-response';
Expand Down
121 changes: 121 additions & 0 deletions packages/php-wasm/universal/src/lib/iterate-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { dirname, joinPaths, normalizePath } from '@php-wasm/util';
import { UniversalPHP } from './universal-php';

export type FileEntry = {
path: string;
isDirectory?: boolean;
bytes: () => Promise<Uint8Array>;
};

export type IterateFilesOptions = {
/**
* Should yield paths relative to the root directory?
* If false, all paths will be absolute.
*/
relativePaths?: boolean;

/**
* A prefix to add to all paths.
* Only used if `relativePaths` is true.
*/
pathPrefix?: string;

/**
* A list of paths to exclude from the results.
*/
exceptPaths?: string[];
};

/**
* Iterates over all files in a php directory and its subdirectories.
*
* @param php - The PHP instance.
* @param root - The root directory to start iterating from.
* @param options - Optional configuration.
* @returns All files found in the tree.
*/
export async function* iterateFiles(
php: UniversalPHP,
root: string,
{
relativePaths = true,
pathPrefix,
exceptPaths = [],
}: IterateFilesOptions = {}
): AsyncGenerator<FileEntry> {
root = normalizePath(root);
const stack: string[] = [root];
while (stack.length) {
const currentParent = stack.pop();
if (!currentParent) {
return;
}
const files = await php.listFiles(currentParent);
for (const file of files) {
const absPath = `${currentParent}/${file}`;
if (exceptPaths.includes(absPath.substring(root.length + 1))) {
continue;
}
const isDir = await php.isDir(absPath);
if (isDir) {
stack.push(absPath);
} else {
yield {
path: relativePaths
? joinPaths(
pathPrefix || '',
absPath.substring(root.length + 1)
)
: absPath,
bytes: async () => await php.readFileAsBuffer(absPath),
};
}
}
}
}

export async function readAllBytes(
reader: ReadableStreamDefaultReader<Uint8Array>
): Promise<Uint8Array> {
let size = 0;
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
size += value.length;
}
const result = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}

/**
* Replaces the contents of a Playground directory with the given files.
*
* @param client
* @param root
* @param newFiles
*/
export async function writeToPath(
client: UniversalPHP,
root: string,
files: AsyncIterable<FileEntry>
) {
await client.mkdir(root);
for await (const file of files) {
const filePath = joinPaths(root, file.path);
if (file.isDirectory) {
await client.mkdir(filePath);
} else {
await client.mkdir(dirname(filePath));
await client.writeFile(filePath, await file.bytes());
}
}
}
2 changes: 1 addition & 1 deletion packages/php-wasm/util/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "../universal/src/lib/iterate-files.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
1 change: 1 addition & 0 deletions packages/playground/blueprints/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './lib/steps';
export * from './lib/steps/handlers';
export * from './lib/zip';
export { runBlueprintSteps, compileBlueprint } from './lib/compile';
export type { Blueprint } from './lib/blueprint';
export type {
Expand Down
34 changes: 32 additions & 2 deletions packages/playground/blueprints/src/lib/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
cloneResponseMonitorProgress,
ProgressTracker,
} from '@php-wasm/progress';
import { UniversalPHP } from '@php-wasm/universal';
import { readAllBytes, UniversalPHP } from '@php-wasm/universal';
import { Semaphore } from '@php-wasm/util';
import { File, zipNameToHumanName } from './steps/common';

Expand Down Expand Up @@ -220,7 +220,12 @@ export abstract class FetchResource extends Resource {
if (response.status !== 200) {
throw new Error(`Could not download "${url}"`);
}
return new File([await response.blob()], this.name);

return new StreamedFile(
response.body!,
this.name,
response.headers.get('content-type') ?? undefined
);
}

/**
Expand Down Expand Up @@ -410,3 +415,28 @@ export class SemaphoreResource<
return this.semaphore.run(() => super.resolve());
}
}

export class StreamedFile extends File {
constructor(
private readableStream: ReadableStream<Uint8Array>,
name: string,
type?: string
) {
super([], name, { type });
}

override slice(): Blob {
throw new Error('Not implemented');
}

override stream() {
return this.readableStream;
}

override async text() {
return new TextDecoder().decode(await this.arrayBuffer());
}
override async arrayBuffer() {
return await readAllBytes(this.readableStream.getReader());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { StepHandler } from '.';
import { unzip } from './unzip';
import { dirname, joinPaths, phpVar } from '@php-wasm/util';
import { wpContentFilesExcludedFromExport } from './common';
import { UniversalPHP } from '@php-wasm/universal';
import { FileEntry, UniversalPHP } from '@php-wasm/universal';

/**
* @inheritDoc importWordPressFiles
Expand All @@ -24,7 +24,8 @@ export interface ImportWordPressFilesStep<ResourceType> {
* The zip file containing the top-level WordPress files and
* directories.
*/
wordPressFilesZip: ResourceType;
wordPressFilesZip?: ResourceType;
files?: AsyncIterable<FileEntry>;
/**
* The path inside the zip file where the WordPress files are.
*/
Expand All @@ -46,11 +47,11 @@ export interface ImportWordPressFilesStep<ResourceType> {
*/
export const importWordPressFiles: StepHandler<
ImportWordPressFilesStep<File>
> = async (playground, { wordPressFilesZip, pathInZip = '' }) => {
> = async (playground, { wordPressFilesZip, files, pathInZip = '' }) => {
const zipPath = '/import.zip';
await playground.writeFile(
zipPath,
new Uint8Array(await wordPressFilesZip.arrayBuffer())
new Uint8Array(await wordPressFilesZip!.arrayBuffer())
);

const documentRoot = await playground.documentRoot;
Expand Down
108 changes: 35 additions & 73 deletions packages/playground/blueprints/src/lib/steps/install-asset.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { UniversalPHP } from '@php-wasm/universal';
import { writeFile } from './write-file';
import { unzip } from './unzip';
import { writeToPath, type UniversalPHP, FileEntry } from '@php-wasm/universal';
import { dirname, joinPaths } from '@php-wasm/util';

export interface InstallAssetOptions {
/**
* The zip file to install.
* The files to install.
*/
zipFile: File;
files: AsyncIterable<FileEntry>;
/**
* The default name of the asset.
* Used if the zip file contains more than one folder.
*/
defaultAssetName: string;
/**
* Target path to extract the main folder.
* @example
Expand All @@ -23,75 +27,33 @@ export interface InstallAssetOptions {
*/
export async function installAsset(
playground: UniversalPHP,
{ targetPath, zipFile }: InstallAssetOptions
): Promise<{
assetFolderPath: string;
assetFolderName: string;
}> {
// Extract to temporary folder so we can find asset folder name

const zipFileName = zipFile.name;
const assetNameGuess = zipFileName.replace(/\.zip$/, '');

const tmpUnzippedFilesPath = `/tmp/assets/${assetNameGuess}`;
const tmpZipPath = `/tmp/${zipFileName}`;

const removeTmpFolder = () =>
playground.rmdir(tmpUnzippedFilesPath, {
recursive: true,
});

if (await playground.fileExists(tmpUnzippedFilesPath)) {
await removeTmpFolder();
}

await writeFile(playground, {
path: tmpZipPath,
data: zipFile,
});

const cleanup = () =>
Promise.all([removeTmpFolder, () => playground.unlink(tmpZipPath)]);

try {
await unzip(playground, {
zipPath: tmpZipPath,
extractToPath: tmpUnzippedFilesPath,
});

// Find the path asset folder name
const files = await playground.listFiles(tmpUnzippedFilesPath, {
prependPath: true,
});
{ targetPath, files, defaultAssetName }: InstallAssetOptions
): Promise<string> {
const extractionPath = joinPaths(targetPath, crypto.randomUUID());
await writeToPath(playground, extractionPath, files);
return await flattenDirectory(playground, extractionPath, defaultAssetName);
}

/**
* If the zip only contains a single entry that is directory,
* we assume that's the asset folder. Otherwise, the zip
* probably contains the plugin files without an intermediate folder.
*/
const zipHasRootFolder =
files.length === 1 && (await playground.isDir(files[0]));
let assetFolderName;
let tmpAssetPath = '';
if (zipHasRootFolder) {
tmpAssetPath = files[0];
assetFolderName = files[0].split('/').pop()!;
} else {
tmpAssetPath = tmpUnzippedFilesPath;
assetFolderName = assetNameGuess;
export async function flattenDirectory(
php: UniversalPHP,
directoryPath: string,
defaultName: string
) {
const parentPath = dirname(directoryPath);

const filesInside = await php.listFiles(directoryPath);
if (filesInside.length === 1) {
const onlyFilePath = joinPaths(directoryPath, filesInside[0]);
const isDir = await php.isDir(onlyFilePath);
if (isDir) {
const finalPath = joinPaths(parentPath, filesInside[0]);
await php.mv(onlyFilePath, finalPath);
await php.rmdir(directoryPath, { recursive: true });
return finalPath;
}

// Move asset folder to target path
const assetFolderPath = `${targetPath}/${assetFolderName}`;
await playground.mv(tmpAssetPath, assetFolderPath);
await cleanup();

return {
assetFolderPath,
assetFolderName,
};
} catch (error) {
await cleanup();
throw error;
}

const finalPath = joinPaths(parentPath, defaultName);
await php.mv(directoryPath, finalPath);
return finalPath;
}
Loading