diff --git a/.cloudflare/pages/functions/[[path]].js b/.cloudflare/pages/functions/[[path]].js index 10aaf1538..7bcde13ce 100644 --- a/.cloudflare/pages/functions/[[path]].js +++ b/.cloudflare/pages/functions/[[path]].js @@ -31,12 +31,43 @@ export const onRequest = async (context) => { return new Response("Not found", { status: 404 }); } - // return new Response(JSON.stringify({key, exists: !isMissing(obj), obj})); - - return new Response(obj.body, { - headers: { - "Content-Type": obj.httpMetadata?.contentType || "application/octet-stream", - "Access-Control-Allow-Origin": "*", - }, - }); + // Preserve original content-type for use after negotiating compression + const originalContentType = obj.httpMetadata?.contentType || "application/octet-stream"; + + // Negotiate pre-compressed variants: Brotli preferred, then gzip + const acceptEncoding = context.request.headers.get("Accept-Encoding") || ""; + let encoding; + + if (acceptEncoding.includes("br")) { + const brKey = key + ".br"; + const brObj = await context.env.NIGHTLY_BUILDS.get(brKey); + if (!isMissing(brObj)) { + obj = brObj; + encoding = "br"; + } + } + + // Fallback to gzip if supported + if (!encoding && acceptEncoding.includes("gzip")) { + const gzKey = key + ".gz"; + const gzObj = await context.env.NIGHTLY_BUILDS.get(gzKey); + if (!isMissing(gzObj)) { + obj = gzObj; + encoding = "gzip"; + } + } + + console.log({originalContentType, encoding}); + + const headers = { + "Content-Type": originalContentType, + "Access-Control-Allow-Origin": "*", + }; + if (encoding) { + headers["Content-Encoding"] = encoding; + headers["Cache-Control"] = "no-transform"; + headers["Vary"] = "Accept-Encoding"; + } + + return new Response(obj.body, { headers, encodeBody: "manual" }); }; diff --git a/.github/.env_8.0.dynamic.ci b/.github/.env_8.0.dynamic.ci index 6427443b1..e1b4eb243 100644 --- a/.github/.env_8.0.dynamic.ci +++ b/.github/.env_8.0.dynamic.ci @@ -6,5 +6,6 @@ ASSERTIONS=0 OPTIMIZE=3 SYMBOLS=0 +WITH_WAITLINE=1 WITH_PDO_PGLITE=0 WITH_VRZNO=0 diff --git a/.github/.env_8.0.shared.ci b/.github/.env_8.0.shared.ci index e18d699ca..9a35079d7 100644 --- a/.github/.env_8.0.shared.ci +++ b/.github/.env_8.0.shared.ci @@ -38,5 +38,6 @@ WITH_ONIGURUMA=shared WITH_OPENSSL=shared WITH_INTL=shared +WITH_WAITLINE=1 WITH_PDO_PGLITE=0 WITH_VRZNO=0 diff --git a/.github/.env_8.0.static.ci b/.github/.env_8.0.static.ci index 0b70ab7ab..6b928d24d 100644 --- a/.github/.env_8.0.static.ci +++ b/.github/.env_8.0.static.ci @@ -38,5 +38,6 @@ WITH_ONIGURUMA=static WITH_OPENSSL=1 WITH_INTL=static +WITH_WAITLINE=1 WITH_PDO_PGLITE=0 WITH_VRZNO=0 diff --git a/.github/.env_8.1.dynamic.ci b/.github/.env_8.1.dynamic.ci index 22ad9aa90..1e5b8c7d7 100644 --- a/.github/.env_8.1.dynamic.ci +++ b/.github/.env_8.1.dynamic.ci @@ -5,3 +5,5 @@ WITH_SOURCEMAPS=0 ASSERTIONS=0 OPTIMIZE=3 SYMBOLS=0 + +WITH_WAITLINE=1 diff --git a/.github/.env_8.1.shared.ci b/.github/.env_8.1.shared.ci index 02ebb2388..b31ad648e 100644 --- a/.github/.env_8.1.shared.ci +++ b/.github/.env_8.1.shared.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=shared WITH_OPENSSL=shared WITH_INTL=shared + +WITH_WAITLINE=1 diff --git a/.github/.env_8.1.static.ci b/.github/.env_8.1.static.ci index 1cb5d5d2d..0d2b4294e 100644 --- a/.github/.env_8.1.static.ci +++ b/.github/.env_8.1.static.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=static WITH_OPENSSL=1 WITH_INTL=static + +WITH_WAITLINE=1 diff --git a/.github/.env_8.2.dynamic.ci b/.github/.env_8.2.dynamic.ci index 23589edcb..4be9e9bc1 100644 --- a/.github/.env_8.2.dynamic.ci +++ b/.github/.env_8.2.dynamic.ci @@ -5,3 +5,5 @@ WITH_SOURCEMAPS=0 ASSERTIONS=0 OPTIMIZE=3 SYMBOLS=0 + +WITH_WAITLINE=1 diff --git a/.github/.env_8.2.shared.ci b/.github/.env_8.2.shared.ci index a98792071..919e8ec5e 100644 --- a/.github/.env_8.2.shared.ci +++ b/.github/.env_8.2.shared.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=shared WITH_OPENSSL=shared WITH_INTL=shared + +WITH_WAITLINE=1 diff --git a/.github/.env_8.2.static.ci b/.github/.env_8.2.static.ci index 9f936d7ab..7c1e18c62 100644 --- a/.github/.env_8.2.static.ci +++ b/.github/.env_8.2.static.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=static WITH_OPENSSL=1 WITH_INTL=static + +WITH_WAITLINE=1 diff --git a/.github/.env_8.3.dynamic.ci b/.github/.env_8.3.dynamic.ci index 48a512900..8cab25521 100644 --- a/.github/.env_8.3.dynamic.ci +++ b/.github/.env_8.3.dynamic.ci @@ -5,3 +5,5 @@ WITH_SOURCEMAPS=0 ASSERTIONS=0 OPTIMIZE=3 SYMBOLS=0 + +WITH_WAITLINE=1 diff --git a/.github/.env_8.3.shared.ci b/.github/.env_8.3.shared.ci index 2d517356b..d0d126f09 100644 --- a/.github/.env_8.3.shared.ci +++ b/.github/.env_8.3.shared.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=shared WITH_OPENSSL=shared WITH_INTL=shared + +WITH_WAITLINE=1 diff --git a/.github/.env_8.3.static.ci b/.github/.env_8.3.static.ci index 1e41bd621..726cab062 100644 --- a/.github/.env_8.3.static.ci +++ b/.github/.env_8.3.static.ci @@ -38,4 +38,4 @@ WITH_ONIGURUMA=static WITH_OPENSSL=1 WITH_INTL=static - +WITH_WAITLINE=1 diff --git a/.github/.env_8.4.dynamic.ci b/.github/.env_8.4.dynamic.ci index 3566fed86..ccf18292a 100644 --- a/.github/.env_8.4.dynamic.ci +++ b/.github/.env_8.4.dynamic.ci @@ -5,3 +5,5 @@ WITH_SOURCEMAPS=0 ASSERTIONS=0 OPTIMIZE=3 SYMBOLS=0 + +WITH_WAITLINE=1 diff --git a/.github/.env_8.4.shared.ci b/.github/.env_8.4.shared.ci index 12793d44e..4e72bbb8f 100644 --- a/.github/.env_8.4.shared.ci +++ b/.github/.env_8.4.shared.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=shared WITH_OPENSSL=shared WITH_INTL=shared + +WITH_WAITLINE=1 diff --git a/.github/.env_8.4.static.ci b/.github/.env_8.4.static.ci index 2a55a8df5..cab51e993 100644 --- a/.github/.env_8.4.static.ci +++ b/.github/.env_8.4.static.ci @@ -37,3 +37,5 @@ WITH_MBSTRING=static WITH_ONIGURUMA=static WITH_OPENSSL=1 WITH_INTL=static + +WITH_WAITLINE=1 diff --git a/.github/bin/index-dirs.sh b/.github/bin/index-dirs.sh index 38c35b753..863af05a3 100755 --- a/.github/bin/index-dirs.sh +++ b/.github/bin/index-dirs.sh @@ -9,10 +9,10 @@ TREE_FLAGS='-FCL 1' cd ${PACKAGES_DIR}; find . -type d | while read DIR; do { - [[ "${DIR::-1}" == "pdo-cfd1" ]] && continue; - [[ "${DIR::-1}" == "pdo-pglite" ]] && continue; - [[ "${DIR::-1}" == "vrzno" ]] && continue; - [[ "${DIR::-1}" == "waitline" ]] && continue; + [[ "${DIR:2}" == "pdo-cfd1" ]] && continue; + [[ "${DIR:2}" == "pdo-pglite" ]] && continue; + [[ "${DIR:2}" == "vrzno" ]] && continue; + [[ "${DIR:2}" == "waitline" ]] && continue; test -d ${DIR} || continue; pushd ${DIR} > /dev/null; @@ -21,6 +21,20 @@ find . -type d | while read DIR; do { perl -pi -e "s#^##" index.html perl -pi -e "s#^

#at $(date)

#" index.html perl -pi -e "s#\t

#\t

php-wasm © 2021-$(date +%Y) Sean Morris

#" index.html + shopt -s nullglob + for BINARY in *.wasm; do + brotli -kfZ ${BINARY} + gzip -k9 ${BINARY} + done; + for BINARY in *.so; do + brotli -kfZ ${BINARY} + gzip -k9 ${BINARY} + done; + for DAT in *.dat; do + brotli -kfZ ${DAT} + gzip -k9 ${DAT} + done; + shopt -u nullglob popd > /dev/null; }; done; diff --git a/.github/workflows/build-step.yaml b/.github/workflows/build-step.yaml index e19325a5b..6ac54299a 100644 --- a/.github/workflows/build-step.yaml +++ b/.github/workflows/build-step.yaml @@ -88,6 +88,9 @@ jobs: - name: Build PHP for Web & PHP-CGI for Worker run: time make --debug=basic web-mjs worker-cgi-mjs + - name: Build PHP CGI for Node + run: time make --debug=basic node-cgi-mjs + - name: Build php${{ inputs.phpVersion }}-sdl.so run: time make --debug=basic packages/sdl/php${{ inputs.phpVersion }}-sdl.so packages/sdl/libGL.so PHP_VERSION=${{ inputs.phpVersion }} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c0d03b2a3..4329b8cf1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,9 @@ name: Build Artifacts -on: [push] +on: + push: + paths-ignore: + - '.cloudflare/pages/functions/\[\[path\]\].js' permissions: contents: read @@ -253,12 +256,54 @@ jobs: + test-cgi-node-dynamic: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs + needs: [build-php-dynamic] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['dynamic'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-uncompressed-*-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + test-cgi-node-shared: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs + needs: [build-php-shared] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['shared'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-uncompressed-*-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + test-cgi-node-static: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs + needs: [build-php-static] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['static'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-uncompressed-*-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + + compress-dynamic-packages: name: Compress PHP w/${{ matrix.libType }} libs runs-on: ubuntu-latest needs: - test-node-dynamic - test-browser-dynamic + - test-cgi-node-dynamic strategy: matrix: libType: ['dynamic'] @@ -324,6 +369,7 @@ jobs: needs: - test-node-shared - test-browser-shared + - test-cgi-node-shared strategy: matrix: libType: ['shared'] @@ -389,6 +435,7 @@ jobs: needs: - test-node-static - test-browser-static + - test-cgi-node-static strategy: matrix: libType: ['static'] @@ -539,12 +586,55 @@ jobs: + test-cgi-node-dynamic-compressed: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs (compressed) + needs: [compress-dynamic-packages] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['dynamic'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-compressed-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + test-cgi-node-shared-compressed: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs (compressed) + needs: [compress-shared-packages] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['shared'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-compressed-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + test-cgi-node-static-compressed: + name: Test CGI Node ${{ matrix.phpVersion }} w/${{ matrix.libType }} libs (compressed) + needs: [compress-static-packages] + strategy: + matrix: + phpVersion: ['8.4', '8.3', '8.2', '8.1', '8.0'] + libType: ['static'] + uses: ./.github/workflows/test-cgi-node-step.yaml + with: + artifactPattern: php-compressed-${{ matrix.libType }} + phpVersion: ${{ matrix.phpVersion }} + libType: ${{ matrix.libType }} + + + index-packages: - name: Create HTML Indexes + name: Post nightly build to Cloudflare + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' runs-on: ubuntu-latest needs: - test-browser-dynamic-compressed - test-node-dynamic-compressed + - test-cgi-node-dynamic-compressed steps: - name: Check out repository code uses: actions/checkout@v4 @@ -567,6 +657,9 @@ jobs: tar -xzf ${TAR} done; + - name: Install Brotli & GZip + run: sudo apt install brotli gzip + - name: Index Package Directories run: ./.github/bin/index-dirs.sh packages/ @@ -601,9 +694,9 @@ jobs: r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} - r2-bucket: ${{ secrets.R2_BUCKET }} - source-dir: packages destination-dir: ${{ env.STAMP }}-${{ env.SHORT_SHA }}/ + r2-bucket: php-wasm + source-dir: packages - name: Install Wrangler run: npm install -g wrangler @@ -620,12 +713,13 @@ jobs: if: ${{ success() }} run: | curl -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": \"New nightly build available: https://nightly.php-wasm.seanmorr.is/${{ env.STAMP }}-${{ env.SHORT_SHA }}/\"}" \ - ${{ secrets.DISCORD_WEBHOOK_URL }} + -X POST \ + -d "{\"content\": \"New nightly build available: https://nightly.php-wasm.seanmorr.is/${{ env.STAMP }}-${{ env.SHORT_SHA }}/\"}" \ + ${{ secrets.DISCORD_WEBHOOK_URL }} build-demo: name: Build Web Demo + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' runs-on: ubuntu-latest needs: - index-packages @@ -696,10 +790,10 @@ jobs: deploy-docs: name: Deploy Docs to GitHub Pages + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' runs-on: ubuntu-latest needs: - build-demo - if: github.ref == 'refs/heads/master' permissions: contents: read pages: write diff --git a/.github/workflows/deploy-pages-function.yaml b/.github/workflows/deploy-pages-function.yaml new file mode 100644 index 000000000..e103a79e3 --- /dev/null +++ b/.github/workflows/deploy-pages-function.yaml @@ -0,0 +1,30 @@ +name: Deploy Cloudflare Pages Function + +on: + push: + paths: + - '.cloudflare/pages/functions/\[\[path\]\].js' + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy-pages-function: + name: Deploy Cloudflare Pages Function + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Wrangler + run: npm install -g wrangler + + - name: Deploy Cloudflare Pages Function + run: | + cd .cloudflare/pages + wrangler pages deploy --branch=main --project-name=php-wasm-nightly + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PAGES_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} \ No newline at end of file diff --git a/.github/workflows/test-cgi-node-step.yaml b/.github/workflows/test-cgi-node-step.yaml new file mode 100644 index 000000000..6d32f75ba --- /dev/null +++ b/.github/workflows/test-cgi-node-step.yaml @@ -0,0 +1,72 @@ +name: Test CGI Node + +on: + workflow_call: + inputs: + phpVersion: + required: true + type: string + libType: + required: true + type: string + artifactPattern: + required: true + type: string + +jobs: + test: + name: Test Cgi Node ${{ inputs.phpVersion }} w/${{ inputs.libType }} libs + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Disable man-db auto-update + run: | + echo "set man-db/auto-update false" | sudo debconf-communicate + sudo dpkg-reconfigure man-db + + - name: Import configuration + run: | + cp -p .github/.env_${{ inputs.phpVersion }}.${{ inputs.libType }}.ci .env + touch -d '1970-01-01 00:00:00 UTC' .env + + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + pattern: ${{ inputs.artifactPattern }} + merge-multiple: true + path: ./ + + - name: Extract Artifact + run: | + for TAR in ${{ inputs.artifactPattern }}.tar.gz; do + tar -xzf ${TAR} + done; + + - name: Install NPM packages + run: npm ci + + - name: Install Global NPM packages + run: npm install -g deno cv3-test netcat + + - name: Install docker-compose + run: sudo apt update && sudo apt install docker-compose -y + + - name: Run tests + run: BUILD_TYPE=${{ inputs.libType }} make --debug=basic test-cgi-node + + - name: Take Failure Snapshot + if: ${{ failure() }} + run: | + sudo chown -R ${USER}:${USER} . + sudo chmod -R u+rwX,go+rX . + tar --exclude=.git --exclude='snapshot.tar.gz' --ignore-failed-read --warning=no-file-changed -czf snapshot.tar.gz . + + - name: Upload Failure Artifact + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: failure-snapshot-cgi-node-test-${{ inputs.phpVersion }}-${{ inputs.libType }} + path: snapshot.tar.gz + compression-level: 0 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0313408f9..3688902f4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,9 @@ name: Test -on: [push] +on: + push: + paths-ignore: + - '.cloudflare/pages/functions/\[\[path\]\].js' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.gitignore b/.gitignore index 88b877291..03ca0e549 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,14 @@ /php*.js /php*.mjs -/php*.js.br -/php*.mjs.br -/php*.js.gz -/php*.mjs.gz /php*.wasm /php*.wat -/php-*.wasm.gz -/php-*.wasm.br /php*.wasm.map /php*.data -/php*.data.gz -/php*.data.br docs-source/app/assets/php*.js docs-source/app/assets/php*.wasm -docs-source/app/assets/php*.wasm.gz -docs-source/app/assets/php*.wasm.br docs-source/app/assets/php*.wat docs-source/app/assets/php*.wasm.map docs-source/app/assets/php*.data -docs-source/app/assets/php*.data.gz -docs-source/app/assets/php*.data.br /Php*.js /Php*.mjs /*.js @@ -61,3 +49,5 @@ demo-web/public/static/media/mapped/* packages/**/index.html packages/*.tar.gz packages/dependencies.json +*.gz +*.br \ No newline at end of file diff --git a/Makefile b/Makefile index 2b12a8bd9..479621ac0 100755 --- a/Makefile +++ b/Makefile @@ -1025,6 +1025,9 @@ test-deno: node-mjs test-browser: PHP_VERSION=${PHP_VERSION} PHP_VARIANT=${PHP_VARIANT} BUILD_TYPE=${BUILD_TYPE} REACT_APP_BUILD_TYPE=${BUILD_TYPE} test/browser-test.sh +test-cgi-node: + PHP_VERSION=${PHP_VERSION} BUILD_TYPE=${BUILD_TYPE} test/node-cgi-test.sh + update-snapshots: PHP_VERSION=${PHP_VERSION} PHP_VARIANT=${PHP_VARIANT} BUILD_TYPE=${BUILD_TYPE} REACT_APP_BUILD_TYPE=${BUILD_TYPE} CV_UPDATE_SNAPSHOTS=1 test/browser-test.sh @@ -1065,12 +1068,6 @@ php-clean-all-versions: ${MAKE} php-clean PHP_VERSION=8.0 demo-versions: - rm -f third_party/php8.4-src/configured - rm -f third_party/php8.3-src/configured - rm -f third_party/php8.2-src/configured - rm -f third_party/php8.1-src/configured - rm -f third_party/php8.0-src/configured - ${MAKE} web-mjs PHP_VERSION=8.4 WITH_SDL=1 ${MAKE} web-mjs PHP_VERSION=8.3 WITH_SDL=1 ${MAKE} web-mjs PHP_VERSION=8.2 WITH_SDL=1 diff --git a/demo-node/index.mjs b/demo-node/index.mjs index afc98db34..6af260708 100755 --- a/demo-node/index.mjs +++ b/demo-node/index.mjs @@ -2,26 +2,67 @@ import http from 'node:http'; import { PhpCgiNode } from 'php-cgi-wasm/PhpCgiNode.mjs'; +const buildType = process.env.BUILD_TYPE ?? 'dynamic'; +const sharedLibs = []; + +if(buildType === 'dynamic') +{ + sharedLibs.push( + await import('php-wasm-intl') + , await import('php-wasm-libxml') + , await import('php-wasm-phar') + , await import('php-wasm-mbstring') + , await import('php-wasm-openssl') + , await import('php-wasm-dom') + , await import('php-wasm-xml') + , await import('php-wasm-simplexml') + , await import('php-wasm-sqlite') + , await import('php-wasm-zlib') + , await import('php-wasm-gd') + ); +} +else if(buildType === 'shared') +{ + sharedLibs.push( + {name: 'libxml2.so', url: new URL('php-wasm-libxml/libxml2.so', import.meta.url)}, + {name: 'libz.so', url: new URL('php-wasm-zlib/libz.so', import.meta.url)}, + {name: 'libzip.so', url: new URL('php-wasm-libzip/libzip.so', import.meta.url)}, + {name: 'libfreetype.so', url: new URL('php-wasm-gd/libfreetype.so', import.meta.url)}, + {name: 'libjpeg.so', url: new URL('php-wasm-gd/libjpeg.so', import.meta.url)}, + {name: 'libwebp.so', url: new URL('php-wasm-gd/libwebp.so', import.meta.url)}, + {name: 'libpng.so', url: new URL('php-wasm-gd/libpng.so', import.meta.url)}, + {name: 'libiconv.so', url: new URL('php-wasm-iconv/libiconv.so', import.meta.url)}, + {name: 'libicuuc.so', url: new URL('php-wasm-intl/libicuuc.so', import.meta.url)}, + {name: 'libicutu.so', url: new URL('php-wasm-intl/libicutu.so', import.meta.url)}, + {name: 'libicutest.so', url: new URL('php-wasm-intl/libicutest.so', import.meta.url)}, + {name: 'libicuio.so', url: new URL('php-wasm-intl/libicuio.so', import.meta.url)}, + {name: 'libicui18n.so', url: new URL('php-wasm-intl/libicui18n.so', import.meta.url)}, + {name: 'libicudata.so', url: new URL('php-wasm-intl/libicudata.so', import.meta.url)}, + {name: 'libcrypto.so', url: new URL('php-wasm-openssl/libcrypto.so', import.meta.url)}, + {name: 'libssl.so', url: new URL('php-wasm-openssl/libssl.so', import.meta.url)}, + {name: 'libonig.so', url: new URL('php-wasm-mbstring/libonig.so', import.meta.url)}, + {name: 'libsqlite3.so', url: new URL('php-wasm-sqlite/libsqlite3.so', import.meta.url)}, + {name: 'libtidy.so', url: new URL('php-wasm-tidy/libtidy.so', import.meta.url)}, + {name: 'libyaml.so', url: new URL('php-wasm-yaml/libyaml.so', import.meta.url)}, + ); +} +if(buildType === 'static') +{ + sharedLibs.push( + {name: 'libcrypto.so', url: (new URL('php-wasm-openssl/libcrypto.so', import.meta.url))}, + {name: 'libssl.so', url: (new URL('php-wasm-openssl/libssl.so', import.meta.url))}, + ); +} + const php = new PhpCgiNode({ - prefix: '/php-wasm/cgi-bin/' + version: process.env.PHP_VERSION ?? '8.3' + , sharedLibs + , prefix: '/php-wasm/cgi-bin/' , docroot: '/persist/www' , persist: [ {mountPath: '/persist' , localPath: './persist'} , {mountPath: '/config' , localPath: './config'} ] - , sharedLibs: [ - // await import('php-wasm-intl') - // , await import('php-wasm-libxml') - // , await import('php-wasm-phar') - // , await import('php-wasm-mbstring') - // , await import('php-wasm-openssl') - // , await import('php-wasm-dom') - // , await import('php-wasm-xml') - // , await import('php-wasm-simplexml') - // , await import('php-wasm-sqlite') - // , await import('php-wasm-zlib') - // , await import('php-wasm-gd') - ] , types: { jpeg: 'image/jpeg' , jpg: 'image/jpeg' diff --git a/demo-node/package-lock.json b/demo-node/package-lock.json index 733c3456c..3d52a8d17 100644 --- a/demo-node/package-lock.json +++ b/demo-node/package-lock.json @@ -31,30 +31,39 @@ } }, "../packages/dom": { + "name": "php-wasm-dom", "version": "0.0.9-alpha-33" }, "../packages/gd": { + "name": "php-wasm-gd", "version": "0.0.9-alpha-33" }, "../packages/iconv": { + "name": "php-wasm-iconv", "version": "0.0.9-alpha-33" }, "../packages/intl": { + "name": "php-wasm-intl", "version": "0.0.9-alpha-33" }, "../packages/libxml": { + "name": "php-wasm-libxml", "version": "0.0.9-alpha-33" }, "../packages/libyaml": { + "name": "php-wasm-yaml", "version": "0.0.9-alpha-33" }, "../packages/libzip": { + "name": "php-wasm-libzip", "version": "0.0.9-alpha-33" }, "../packages/mbstring": { + "name": "php-wasm-mbstring", "version": "0.0.9-alpha-33" }, "../packages/openssl": { + "name": "php-wasm-openssl", "version": "0.0.9-alpha-33" }, "../packages/pdo-pglite": { @@ -64,6 +73,7 @@ } }, "../packages/phar": { + "name": "php-wasm-phar", "version": "0.0.9-alpha-33" }, "../packages/php-cgi-wasm": { @@ -83,18 +93,23 @@ "license": "Apache-2.0" }, "../packages/simplexml": { + "name": "php-wasm-simplexml", "version": "0.0.9-alpha-33" }, "../packages/sqlite": { + "name": "php-wasm-sqlite", "version": "0.0.9-alpha-33" }, "../packages/tidy": { + "name": "php-wasm-tidy", "version": "0.0.9-alpha-33" }, "../packages/xml": { + "name": "php-wasm-xml", "version": "0.0.9-alpha-33" }, "../packages/zlib": { + "name": "php-wasm-zlib", "version": "0.0.9-alpha-33" }, "node_modules/pdo-pglite": { diff --git a/demo-node/persist/www/index.php b/demo-node/persist/www/index.php index b834b1b2e..7394d45f8 100644 --- a/demo-node/persist/www/index.php +++ b/demo-node/persist/www/index.php @@ -42,6 +42,8 @@
More:
phpinfo
+
version
+
extensions
diff --git a/demo-node/persist/www/test/extensions.php b/demo-node/persist/www/test/extensions.php new file mode 100644 index 000000000..aa562d2cb --- /dev/null +++ b/demo-node/persist/www/test/extensions.php @@ -0,0 +1 @@ +=0.10.0" } @@ -17660,6 +17675,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -17905,6 +17921,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19248,20 +19265,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -19602,6 +19605,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -20040,6 +20044,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -20089,6 +20094,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -20167,6 +20173,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -20601,6 +20608,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/demo-web/package.json b/demo-web/package.json index e05f23386..1a1defd9e 100644 --- a/demo-web/package.json +++ b/demo-web/package.json @@ -3,11 +3,11 @@ "version": "0.0.0", "private": true, "dependencies": { - "ansi-to-html": "^0.7.2", "@electric-sql/pglite": "0.2.17", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "ansi-to-html": "^0.7.2", "pdo-pglite": "../packages/pdo-pglite", "php-cgi-wasm": "../packages/php-cgi-wasm", "php-cli-wasm": "../packages/php-cli-wasm", @@ -65,6 +65,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "baseline-browser-mapping": "^2.9.11", "webpack-cli": "^5.1.4" } } diff --git a/demo-web/public/cgi-worker.js b/demo-web/public/cgi-worker.js index 5d2780caf..bdd7624b3 100644 --- a/demo-web/public/cgi-worker.js +++ b/demo-web/public/cgi-worker.js @@ -321,7 +321,6 @@ class PhpCgiBase { return coordinator; } refresh() { - // const {files, libs, urlLibs} = resolveDependencies(this.sharedLibs, this); const { files: sharedLibFiles, libs: sharedLibs, @@ -332,7 +331,6 @@ class PhpCgiBase { libs: dynamicLibs, urlLibs: dynamicLibUrls } = (0,_resolveDependencies_mjs__WEBPACK_IMPORTED_MODULE_3__.resolveDependencies)(this.dynamicLibs, this); - const files = [...sharedLibFiles, ...dynamicLibFiles]; const userLocateFile = this.phpArgs.locateFile || (() => undefined); const locateFile = (path, directory) => { let located = userLocateFile(path, directory); @@ -351,6 +349,12 @@ class PhpCgiBase { } return String(dynamicLibUrls[path]); } + + // Suppress attempt to load libxml when + // it hasn't been provided in sharedLibs + if (path === 'libxml2.so') { + return 'data:,'; + } }; const phpArgs = { persist: [{ @@ -364,16 +368,16 @@ class PhpCgiBase { stderr: x => this.error.push(x), locateFile }; - return this.binary = this.binLoader.then({ + return this.binary = this.binLoader.then(({ default: PHP - }).then(async php => { + }) => new PHP(phpArgs)).then(async php => { await php.ccall('pib_storage_init', NUM, [], [], { async: true }); if (!php.FS.analyzePath('/preload').exists) { php.FS.mkdir('/preload'); } - const allFiles = this.files.concat(files).concat(sharedLibFiles).concat(dynamicLibFiles); + const allFiles = this.files.concat(sharedLibFiles).concat(dynamicLibFiles); // Make sure folder structure exists before preloading files allFiles.forEach(fileDef => { diff --git a/demo-web/public/index.html b/demo-web/public/index.html index 3305ca4c5..45272df5f 100644 --- a/demo-web/public/index.html +++ b/demo-web/public/index.html @@ -1,80 +1,18 @@ - - - - - - - - php-wasm - - - - - -
- - + + + + + + + + php-wasm + + + + +
+ diff --git a/demo-web/public/scripts/sdl-sine.php b/demo-web/public/scripts/sdl-sine.php index f16ff1a80..809de181f 100644 --- a/demo-web/public/scripts/sdl-sine.php +++ b/demo-web/public/scripts/sdl-sine.php @@ -1,4 +1,4 @@ - s let lastCommand = null; export default forwardRef(function Debugger({ - file, setCurrentFile, setCurrentLine, setStatusMessage - , localEcho = true, initCommands = [], onStdIn + className = '', file, localEcho = true, initCommands = [], onStdIn + , setCurrentFile, setCurrentLine, setStatusMessage, setIsExecuting + , openFile, version = '8.3', }, ref) { const phpRef = useRef(null); const cmdStack = useRef(['']); const cmdStackIndex = useRef(0); const terminal = useRef(''); const stdIn = useRef(''); + const init = useRef(true); + const [prompt, setPrompt] = useState(parser.toHtml(escapeHtml('\x1b[1mprompt> '))); const [ready, setReady] = useState(false); const [output, setOutput] = useState([]); const [exitCode, setExitCode] = useState(''); - const init = useRef(true); + + const [variables, setVariables] = useState({}); + const [globals, setGlobals] = useState({}); + const [constants, setConstants] = useState({}); + const [functions, setFunctions] = useState({}); + const [userClasses, setUserClasses] = useState({}); + + const [includedFiles, setIncludedFiles] = useState([]); + const [trace, setTrace] = useState([]); + const [currentFrame, setCurrentFrame] = useState(0); + + const [currentPanel, setCurrentPanel] = useState('none'); const query = useMemo(() => new URLSearchParams(window.location.search), []); const [isIframe, setIsIframe] = useState(!!Number(query.get('iframed'))); @@ -166,7 +184,7 @@ export default forwardRef(function Debugger({ const refreshPhp = useCallback(init => { setStatusMessage && setStatusMessage('loading...'); phpRef.current = new PhpDbgWeb({ - version: '8.3', + version, sharedLibs, files, ini, @@ -232,7 +250,9 @@ export default forwardRef(function Debugger({ { event.preventDefault(); - if(event.target.selectionStart > 0) + const end = event.target.value.length; + + if(event.target.selectionStart > 0 && event.target.selectionStart !== end) { event.target.selectionStart = 0; event.target.selectionEnd = 0; @@ -246,11 +266,7 @@ export default forwardRef(function Debugger({ } stdIn.current.value = cmdStack.current[cmdStackIndex.current]; - - event.target.selectionStart = 0; - event.target.selectionEnd = 0; - - return; + stdIn.current.selectionStart = stdIn.current.selectionEnd = stdIn.current.value.length; } if(event.key === 'ArrowDown') @@ -266,15 +282,14 @@ export default forwardRef(function Debugger({ return; } - stdIn.current.value = cmdStack.current[cmdStackIndex.current]; - cmdStackIndex.current++; if(cmdStackIndex.current >= cmdStack.current.length) { cmdStackIndex.current = 0; } - return; + stdIn.current.value = cmdStack.current[cmdStackIndex.current]; + stdIn.current.selectionStart = stdIn.current.selectionEnd = stdIn.current.value.length; } if(event.key === 'Enter') @@ -311,10 +326,69 @@ export default forwardRef(function Debugger({ await php.provideInput(inputValue); - lastCommand = inputValue || lastCommand; - setExitCode(exitCode); + const isRunning = await php.isExecuting(); + + setIsExecuting( isRunning ); + + if(!silent) + { + lastCommand = inputValue || lastCommand; + + stdIn.current && stdIn.current.focus(); + } - stdIn.current && stdIn.current.focus(); + if(isRunning) + { + if(currentPanel === 'none') + { + setCurrentPanel('variables'); + setVariables( await (await phpRef.current).dumpVars() || {} ); + } + + switch(currentPanel) + { + case 'variables': + setVariables( await (await phpRef.current).dumpVars() || {} ); + break; + + case 'globals': + setGlobals( await (await phpRef.current).dumpGlobals() || {} ); + break; + + case 'constants': + setConstants( await (await phpRef.current).dumpConstants() || {} ); + break; + + case 'classes': + setUserClasses( await (await phpRef.current).dumpClasses() || {} ); + break; + + case 'functions': + setFunctions( await (await phpRef.current).dumpFunctions() || {} ); + break; + + case 'files': + setIncludedFiles( await (await phpRef.current).dumpFiles() || [] ); + break; + + case 'trace': + const php = (await phpRef.current); + const oldFrame = await php.switchFrame(0); + setCurrentFrame(oldFrame); + setTrace( await (await phpRef.current).dumpBacktrace() || [] ); + php.switchFrame(oldFrame); + break; + } + } + else + { + setVariables( {} ); + setGlobals( {} ); + setConstants( {} ); + setFunctions( {} ); + setIncludedFiles( [] ); + setCurrentPanel('none'); + } } const focusInput = event => { @@ -348,16 +422,146 @@ export default forwardRef(function Debugger({ scrollToEnd(); }; - return (
-
🡇
-
- ⚠️ This is in VERY early alpha! ⚠️ - {output.map((line, index) => (
))} -
- {!ready && ()} - - - + const zvalViews = obj => { + + try + { + const entries = Object.entries(obj); + return
+ {entries.map(zvalView)} +
; + } + catch(error) + { + console.error(error); + console.warn(obj, 'returned duplicate keys'); + } + }; + + const zvalView = ([name, zv]) => { + return ; + }; + + const switchRightPanel = async panel => { + switch(panel) + { + case undefined: + case 'variables': + setVariables( await (await phpRef.current).dumpVars() || {} ); + setCurrentPanel('variables'); + break; + + case 'globals': + setGlobals( await (await phpRef.current).dumpGlobals() || {} ); + setCurrentPanel('globals'); + break; + + case 'constants': + setConstants( await (await phpRef.current).dumpConstants() || {} ); + setCurrentPanel('constants'); + break; + + case 'classes': + console.log( await (await phpRef.current).dumpClasses() || {} ); + setUserClasses( await (await phpRef.current).dumpClasses() || {} ); + setCurrentPanel('classes'); + break; + + case 'functions': + setFunctions( await (await phpRef.current).dumpFunctions() || {} ); + setCurrentPanel('functions'); + break; + + case 'files': + setIncludedFiles( await (await phpRef.current).dumpFiles() || [] ); + setCurrentPanel('files'); + break; + + case 'trace': + const php = (await phpRef.current); + const oldFrame = await php.switchFrame(0); + console.log(oldFrame); + setCurrentFrame(oldFrame); + setTrace( await (await phpRef.current).dumpBacktrace() || [] ); + setCurrentPanel('trace'); + php.switchFrame(oldFrame); + break; + } + }; + + return (
+
+
+
+
🡇
+ ⚠️ This is in VERY early alpha! ⚠️ + {output.map((line, index) => (
))} +
+ {!ready && ()} + + + +
+
+
+
+
+
+ + + + + + + +
+
+
+
+ {zvalViews(variables)} +
+
+ {zvalViews(globals)} +
+
+ {zvalViews(constants)} +
+
+ {Object.entries(userClasses).sort((a, b) => String(a[0]).localeCompare(b[0])).map(([name,func]) => { + return
openFile(func.filename, func.lineNo)}> + {name} {String(func.filename).split('/').pop()}:{func.lineNo} +
+ })} +
+
+ {Object.entries(functions).sort((a, b) => String(a[0]).localeCompare(b[0])).map(([name,func]) => { + return
openFile(func.filename, func.lineNo)}> + {name} {String(func.filename).split('/').pop()}:{func.lineNo} +
+ })} +
+
+ {includedFiles.sort((a, b) => String(a).localeCompare(b)).map(name => { + return
openFile(name)}> + {name} +
+ })} +
+
+ {trace.map((frame) => { + return
{ + openFile(frame.filename, frame.lineNo); + (await phpRef.current).switchFrame(frame.frame); + setCurrentFrame(frame.frame); + }}> + {frame.filename}: {frame.lineNo} +
+ })} +
+
); diff --git a/demo-web/src/Editor.css b/demo-web/src/Editor.css index 73e5ab1f1..1219f2641 100644 --- a/demo-web/src/Editor.css +++ b/demo-web/src/Editor.css @@ -139,4 +139,8 @@ .editor iframe { flex-grow: 1; +} + +select.bevel option { + background-color: #FFF; } \ No newline at end of file diff --git a/demo-web/src/Editor.js b/demo-web/src/Editor.js index 45bafe7f1..577cdb273 100644 --- a/demo-web/src/Editor.js +++ b/demo-web/src/Editor.js @@ -51,6 +51,7 @@ export default function Editor() { const [openFiles, setOpenFiles] = useState([]); const [showLeft, setShowLeft] = useState([]); const [phpdbg, setPhpDbg] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); const activeLines = useRef(new Set); const currentBreak = useRef({}); @@ -60,6 +61,12 @@ export default function Editor() { const tabBox = useRef(null); const openDbg = useRef(null); + const lastFile = useRef(null); + const lastLine = useRef(null); + + const versionSelector = useRef(true); + const version = useRef('8.3'); + const query = useMemo(() => new URLSearchParams(window.location.search), []); const handleSave = async () => { @@ -170,6 +177,14 @@ export default function Editor() { window.history.replaceState({}, null, window.location.pathname + '?' + query); } + if(currentPath.current !== path) + { + activeLines.current.forEach(m => { + editor.session.removeMarker(m); + activeLines.current.delete(m); + }); + } + currentPath.current = path; editor.setReadOnly(!!openDbg.current); @@ -296,16 +311,51 @@ export default function Editor() { const handleOpenFile = event => openFile(event.detail); + const gotoFile = async (file, line) => { + const { exists } = await sendMessage('analyzePath', [file]); + + const editor = aceRef.current.editor; + + if(exists) + { + await openFile(file); + + editor.scrollToLine(-1 + line, true, true, () => {}); + } + + if(line !== undefined) + { + activeLines.current.forEach(m => { + editor.session.removeMarker(m); + activeLines.current.delete(m); + }); + + const marker = editor.session.addMarker( + new Range(-1 + line, 0, -1 + line, Infinity) + , 'active_breakpoint' + , 'fullLine' + , true + ); + + activeLines.current.add(marker); + } + } + const startDebugger = () => { const editor = aceRef.current.editor; if(openDbg.current) { - activeLines.current.forEach(m => editor.session.removeMarker(m)); + activeLines.current.forEach(m => { + editor.session.removeMarker(m); + activeLines.current.delete(m); + }); + openDbg.current = null; editor.setReadOnly(false); setPhpDbg(openDbg.current); + setIsExecuting(false); return; } @@ -313,27 +363,52 @@ export default function Editor() { openDbg.current = `b ${bp}`), 'run']} setCurrentFile = {file => currentBreak.current.file = file} setCurrentLine = {line => currentBreak.current.line = line} - onStdIn = {async () => { + setIsExecuting = {setIsExecuting} + openFile = {gotoFile} + onStdIn = {async (...args) => { const file = currentBreak.current.file; const line = currentBreak.current.line; - activeLines.current.forEach(m => editor.session.removeMarker(m)); + if(!(file && line)) return; - if(file && line) + if(file === lastFile.current && line === lastLine.current) return; + + const { exists } = await sendMessage('analyzePath', [file]); + + if(exists) { await openFile(file); + activeLines.current.forEach(m => { + editor.session.removeMarker(m); + activeLines.current.delete(m); + }); + const marker = editor.session.addMarker( - new Range(-1 + line, 0, -1 + line, Infinity), 'active_breakpoint', 'fullLine', true + new Range(-1 + line, 0, -1 + line, Infinity) + , 'active_breakpoint' + , 'fullLine' + , true ); + activeLines.current.add(marker); + editor.scrollToLine(-1 + line, true, true, () => {}); - activeLines.current.add(marker); + lastFile.current = file; + lastLine.current = line; + } + else + { + activeLines.current.forEach(m => { + editor.session.removeMarker(m); + activeLines.current.delete(m); + }); } }} />; @@ -364,10 +439,18 @@ export default function Editor() { - {!phpdbg ? ( - + {!isExecuting ? ( + <> + {phpdbg ? '' : } + + ) : (
- {phpdbg && ( -
{phpdbg}
- )} + {phpdbg &&
{phpdbg}
}
diff --git a/demo-web/src/Embedded.css b/demo-web/src/Embedded.css index 431e768a4..6448c6bcc 100644 --- a/demo-web/src/Embedded.css +++ b/demo-web/src/Embedded.css @@ -23,6 +23,42 @@ font-family: 'fixedsys'; } +@media screen and (max-width: 1300px) { + + .Embedded .row.toolbar.header .row { + flex-wrap: wrap; + } + + .Embedded .row.toolbar.header hr, + .Embedded .row.toolbar.header .separator { + display: none; + } + + .Embedded .row.toolbar.header { + /* font-size: 1.5em; */ + } + + .Embedded .row.toolbar input[type="radio"], + .Embedded .row.toolbar input[type="checkbox"] { + transform: scale(1.5); + margin: 10px; + } + + .Embedded .row.toolbar select, + .Embedded .row.toolbar button { + padding: 0.5rem; + margin-top: 0.25rem; + } + + .Embedded .row.toolbar.header { + align-items: flex-start; + } + + .Embedded .row.toolbar.header .rows.spread { + justify-content: flex-start; + } +} + body { font-family: 'fixedsys'; display: flex; @@ -447,3 +483,30 @@ body:not(.loading) .loader { flex-grow: 1; min-width: 100%; } + +div.toggleExtensions { + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +div.toggleExtensions label, div.toggleExtensions button { + padding: 0.5rem; +} + +div.toggleExtensions label { + border: 2px solid; + border-image: url(border-inset.png) 2 2 2 2; +} + +div.toggleExtensions button { + width: 100%; +} + +div.toggleExtensions input { + transform: scale(2); + margin: 0.25rem; + margin-right: 1rem; +} \ No newline at end of file diff --git a/demo-web/src/Embedded.js b/demo-web/src/Embedded.js index 78997f470..219cd5386 100644 --- a/demo-web/src/Embedded.js +++ b/demo-web/src/Embedded.js @@ -22,29 +22,33 @@ const files = [ { parent: '/preload/', name: 'list-extensions.php', url: './scripts/list-extensions.php' }, ]; +let canToggleExtensions = false; +const toggleable = {}; + if(buildType === 'dynamic') { - sharedLibs.push(...(await Promise.all([ - import('php-wasm-libxml'), - import('php-wasm-dom'), - import('php-wasm-xml'), - import('php-wasm-simplexml'), - import('php-wasm-zlib'), - import('php-wasm-libzip'), - import('php-wasm-gd'), - import('php-wasm-iconv'), - import('php-wasm-intl'), - import('php-wasm-openssl'), - import('php-wasm-mbstring'), - import('php-wasm-sqlite'), - ])).map(m => m.default)); + canToggleExtensions = true; + + toggleable['dom'] = {active: false, module: import('php-wasm-dom')}; + toggleable['gd'] = {active: false, module: import('php-wasm-gd')}; + toggleable['iconv'] = {active: false, module: import('php-wasm-iconv')}; + toggleable['intl'] = {active: false, module: import('php-wasm-intl')}; + toggleable['libxml'] = {active: false, module: import('php-wasm-libxml')}; + toggleable['yaml'] = {active: false, module: import('php-wasm-yaml')}; + toggleable['libzip'] = {active: false, module: import('php-wasm-libzip')}; + toggleable['mbstring'] = {active: false, module: import('php-wasm-mbstring')}; + toggleable['openssl'] = {active: false, module: import('php-wasm-openssl')}; + toggleable['simplexml'] = {active: false, module: import('php-wasm-simplexml')}; + toggleable['sqlite'] = {active: false, module: import('php-wasm-sqlite')}; + toggleable['xml'] = {active: false, module: import('php-wasm-xml')}; + toggleable['zlib'] = {active: false, module: import('php-wasm-zlib')}; + + sharedLibs.push(...(await Promise.all( + Object.values(toggleable).filter(t => t.active).map(t => t.module) + )).map(m => m.default)); } else if(buildType === 'shared') { - // files.push( - // { parent: '/preload/', name: 'icudt72l.dat', url: new URL('php-wasm-intl/icudt72l.dat', import.meta.url) } - // ); - sharedLibs.push( {name: 'libxml2.so', url: new URL('php-wasm-libxml/libxml2.so', import.meta.url)}, {name: 'libz.so', url: new URL('php-wasm-zlib/libz.so', import.meta.url)}, @@ -67,6 +71,10 @@ else if(buildType === 'shared') {name: 'libtidy.so', url: new URL('php-wasm-tidy/libtidy.so', import.meta.url)}, {name: 'libyaml.so', url: new URL('php-wasm-yaml/libyaml.so', import.meta.url)}, ); + + // files.push( + // { parent: '/preload/', name: 'icudt72l.dat', url: new URL('php-wasm-intl/icudt72l.dat', import.meta.url) } + // ); } else { @@ -114,7 +122,7 @@ function Embedded() { const [stdErr, setStdErr] = useState(''); const [stdRet, setStdRet] = useState(''); const [overlay, setOverlay] = useState(null); - const [isIframe, setIsIframe] = useState(!!Number(query.get('iframed'))); + const [isIframe, /*setIsIframe*/] = useState(!!Number(query.get('iframed'))); const [showCanvas, setShowCanvas] = useState(true); const [running, setRunning] = useState(false); @@ -126,8 +134,11 @@ function Embedded() { const onError = event => setStdErr(stdErr => String(stdErr || '') + event.detail.join('')); const refreshPhp = useCallback(() => { - const version = (selectVersionBox.current ? selectVersionBox.current.value : '8.4') ?? '8.4'; - const variant = (selectVariantBox.current ? selectVariantBox.current.value : '') ?? ''; + const version = (selectVersionBox.current ? selectVersionBox.current.value : null) + ?? '8.4'; + + const variant = (selectVariantBox.current ? selectVariantBox.current.value : null) + ?? ''; const _sharedLibs = [...sharedLibs]; @@ -159,6 +170,27 @@ function Embedded() { }; }, []); + const loadExtensions = useCallback(async () => { + if(!canToggleExtensions) return; + const defaultExtensions = query.has('extensionFlags') + ? Number(query.get('extensionFlags')) + : 0x1FDF; + + let i = 0; + const toggleableList = Object.values(toggleable); + while(i < toggleableList.length) + { + toggleableList[i].active = !!(defaultExtensions & 2**i); + i++; + } + + sharedLibs.length = 0; + + sharedLibs.push(...(await Promise.all( + Object.values(toggleable).filter(t => t.active).map(t => t.module) + )).map(m => m.default)); + }, [query]); + useEffect(() => { persist.current.checked = !!Number(query.get('persist')) ?? ''; single.current.checked = !!Number(query.get('single-expression')) ?? ''; @@ -167,10 +199,15 @@ function Embedded() { if(!init.current && !query.has('demo')) { - refreshPhp(); + (async () => { + await loadExtensions(); + refreshPhp(); + })(); } + init.current = true; - }, [refreshPhp]); + + }, [refreshPhp, query, loadExtensions]); const singleChanged = () => setOutputMode(single.current.checked ? 'single' : 'normal'); const canvasChanged = () => setShowCanvas(canvasCheckbox.current.checked); @@ -179,7 +216,7 @@ function Embedded() { const refreshMem = useCallback(async () => { await phpRef.current.refresh(); - }); + }, []); const runCode = useCallback(async () => { setRunning(true); @@ -205,6 +242,7 @@ function Embedded() { 'single-expression': single.current?.checked, 'render-as': htmlRadio.current?.checked ? 'html' : 'text', 'canvas': canvasCheckbox.current?.checked, + // 'extensionFlags': 0, 'version': version, 'variant': variant, })}\n`); @@ -263,9 +301,10 @@ function Embedded() { }, [query]); const loadDemo = useCallback(demoName => { - if(!demoName) { + refreshPhp(); + runCode(); return; } @@ -312,6 +351,11 @@ function Embedded() { query.set('persist', persist.current.checked ? 1 : 0); query.set('single-expression', single.current.checked ? 1 : 0); + if('extensionFlags' in settings) + { + query.set('extensionFlags', settings.extensionFlags); + } + if(phpCode.length < 1024) { query.set('code', encodeURIComponent(phpCode)); @@ -319,6 +363,8 @@ function Embedded() { window.history.replaceState({}, document.title, "?" + query.toString()); + phpRef.current && await phpRef.current.refresh(); + await loadExtensions(); refreshPhp(); if(settings.autorun) @@ -330,7 +376,7 @@ function Embedded() { setShowCanvas(canvasCheckbox.current.checked); setOutputMode(single.current.checked ? 'single' : 'normal'); }); - }, [query, refreshPhp, runCode]); + }, [query, refreshPhp, runCode, loadExtensions]); useEffect(() => { if(inputBox.current) @@ -404,27 +450,78 @@ function Embedded() { const demoSelected = () => loadDemo(selectDemoBox.current.value); - const onKeyDown = event => { - if(event.key === 'Enter' && event.ctrlKey) - { - runCode(); - return; - } - }; - useEffect(() => { - + const onKeyDown = event => { + if(event.key === 'Enter' && event.ctrlKey) + { + runCode(); + return; + } + }; window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); } - }, []); + + }, [runCode]); const openFile = async event => { const file = event.target.files[0]; editor.current.editor.setValue(await file.text(), -1);; }; + const toggleExtension = async (event, name) => { + if(!canToggleExtensions) return; + if(!toggleable[name]) + { + console.warn(`${name} is not a valid extension name`); + return; + } + + toggleable[name].active = event.target.checked; + + sharedLibs.length = 0; + + sharedLibs.push(...(await Promise.all( + Object.values(toggleable).filter(t => t.active).map(t => t.module) + )).map(m => m.default)); + }; + + const showExtensionDialog = () => { + if(!canToggleExtensions) return; + setOverlay( +
+
+ {Object.entries(toggleable).map(([name, t]) => { + return + })} +
+ +
+
+
+ ); + }; + + const closeExtensionsDialog = async () => { + if(!canToggleExtensions) return; + const extensionFlags = Object.values(toggleable) + .map((t, k) => !!t.active << k) + .reduce((x, p) => x + p, 0); + + query.set('extensionFlags', extensionFlags); + + setOverlay(null); + setStdOut(''); + setStdErr(''); + await loadExtensions(); + refreshPhp(); + runCode(); + }; + const topBar = (
@@ -439,12 +536,12 @@ function Embedded() { Demo: - + - - + +
+
+ + {canToggleExtensions && ()} +
@@ -513,8 +618,10 @@ function Embedded() {
- - +
+ + +
); @@ -524,8 +631,7 @@ function Embedded() { {statusMessage} -
-
+
); return (
diff --git a/demo-web/src/InstallDemo.js b/demo-web/src/InstallDemo.js index 79774710b..8f5326dff 100644 --- a/demo-web/src/InstallDemo.js +++ b/demo-web/src/InstallDemo.js @@ -70,7 +70,13 @@ export default function InstallDemo() { useEffect(() => void (async()=>{ await navigator.serviceWorker.register(process.env.PUBLIC_URL + `/cgi-worker.js`); - await navigator.serviceWorker.getRegistration(`${window.location.origin}${process.env.PUBLIC_URL}/cgi-worker.mjs`); + await navigator.serviceWorker.getRegistration(`${window.location.origin}${process.env.PUBLIC_URL}/`); + await navigator.serviceWorker.ready; + + await new Promise((resolve) => { + if (navigator.serviceWorker.controller) return resolve(); + navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true }); + }); if(!(navigator.serviceWorker && navigator.serviceWorker.controller)) { @@ -165,7 +171,7 @@ export default function InstallDemo() { setTerminal(
diff --git a/demo-web/src/dbg-preview.css b/demo-web/src/dbg-preview.css index 19930ff2d..b0298e639 100644 --- a/demo-web/src/dbg-preview.css +++ b/demo-web/src/dbg-preview.css @@ -42,23 +42,29 @@ flex: 1; } -.phpdbg-console { +.phpdbg { display: flex; - flex-direction: column; + flex-direction: row; position: absolute; width: 100%;; height: 100%; - background-color: #000; - color: #FFF; flex: 1; box-sizing: border-box; } +.phpdbg-console { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + .phpdbg-console .scroll-to-bottom { z-index: 100; - position: absolute; - top: 1rem; - right: 1.75rem; + position: sticky; + top: 0; + left: calc(100% - 2rem); width: 2rem; height: 2rem; background-color: #888; @@ -72,11 +78,16 @@ } .phpdbg-console .console-output { + color: #FFF; + background-color: #000; + + position: relative; white-space: pre-wrap; overflow-y: scroll; overflow-x: auto; padding: 1rem; - min-height: 100%; + height: 100%; + box-sizing: border-box; text-align: left; } @@ -151,3 +162,150 @@ .phpdbg-console .console-input button:active { filter: brightness(2); } + +.phpdbg .phpdbg-left-panel { + flex-grow: 1; + flex-basis: 50%; + position: relative; +} + +.phpdbg .phpdbg-right-panel { + display: flex; + flex-direction: column; + flex-grow: 1; + flex-basis: 30%; + + background-color: #FFF; +} + +.phpdbg .phpdbg-zval label { + display: flex; + flex-direction: row; + flex-grow: 1; + border-right: 1px solid #CCC; + + width: 100%; +} + +.phpdbg .phpdbg-zval label > div:not(:has(*)) { + padding: 0.25rem; +} + +.phpdbg .phpdbg-zval label .variable-name { + background-color: #CCC; +} + +.phpdbg .phpdbg-zval label .variable-name { + border-top-width: 1px; + border-bottom-width: 1px; +} + +.phpdbg .phpdbg-zval label .variable-value { + background-color: #FFF; + flex-grow: 1; +} + +.phpdbg .phpdbg-zval label { + border-bottom: 1px solid #0004; +} + +.phpdbg .phpdbg-zval label .phpdbg-zval label:last-child { + border-bottom: none; +} + +.editor .phpdbg .phpdbg-right-panel .toolbar { + margin: 0; + background-color: #ccc; + border-top: none; + overflow-x: auto; + width: 100%; + box-sizing: border-box; +} + +.phpdbg-panel { + display: none; + width: 100%; + overflow-y: auto; + overflow-x: auto; +} + +.editor .phpdbg .phpdbg-right-panel[data-current-panel="none"] { + display: none; +} + +[data-current-panel="variables"] .phpdbg-panel.phpdbg-variables { + display: initial; +} + +[data-current-panel="globals"] .phpdbg-panel.phpdbg-globals { + display: initial; +} + +[data-current-panel="constants"] .phpdbg-panel.phpdbg-constants { + display: initial; +} + +[data-current-panel="classes"] .phpdbg-panel.phpdbg-classes { + display: initial; +} + +[data-current-panel="functions"] .phpdbg-panel.phpdbg-functions { + display: initial; +} + +[data-current-panel="files"] .phpdbg-panel.phpdbg-files { + display: initial; +} + +[data-current-panel="trace"] .phpdbg-panel.phpdbg-trace { + display: initial; +} + +.phpdbg-panel.phpdbg-trace .current-frame { + background-color: #008; + color: #FFF; +} + +.phpdbg-panel-frame { + position: relative; + width: 100%; + flex: 1; + box-sizing: border-box; +} + +.phpdbg-panel-frame-inner { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: auto; +} + +.phpdbg-panel.phpdbg-files > div, +.phpdbg-panel.phpdbg-classes > div, +.phpdbg-panel.phpdbg-trace > div, +.phpdbg-panel.phpdbg-functions > div { + padding: 0.5rem; + border-bottom: 1px solid #CCC; + white-space: pre; +} + +.phpdbg-panel.phpdbg-files > div span, +.phpdbg-panel.phpdbg-classes > div span, +.phpdbg-panel.phpdbg-trace > div, +.phpdbg-panel.phpdbg-functions > div > span { + cursor: pointer; +} + +.phpdbg-panel.phpdbg-classes > div .filename, +.phpdbg-panel.phpdbg-functions > div .filename { + color: #666; +} + +.phpdbg-panel.phpdbg-files > div:active span, +.phpdbg-panel.phpdbg-classes > div:active span, +.phpdbg-panel.phpdbg-functions > div:active > span { + color: #999; +} + diff --git a/demo-web/src/index.js b/demo-web/src/index.js index 2c8913492..1a5d87b96 100644 --- a/demo-web/src/index.js +++ b/demo-web/src/index.js @@ -19,13 +19,24 @@ const params = new URLSearchParams(window.location.search); if(!params.has('no-service-worker')) { - navigator.serviceWorker.register(process.env.PUBLIC_URL + `/cgi-worker.js`); - setTimeout(() => { + (async () => { + await navigator.serviceWorker.register(process.env.PUBLIC_URL + `/cgi-worker.js`); + await navigator.serviceWorker.getRegistration(`${window.location.origin}${process.env.PUBLIC_URL}/`); + await navigator.serviceWorker.ready; + + await new Promise((resolve) => { + if (navigator.serviceWorker.controller) return resolve(); + navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true }); + }); + if(!(navigator.serviceWorker && navigator.serviceWorker.controller)) { + console.log('No Service Worker Detected, Reloading...'); + await new Promise(a => setTimeout(a, 500)); window.location.reload(); + return; } - }, 450); + })(); navigator.serviceWorker.addEventListener('message', onMessage); } diff --git a/docs/404.html b/docs/404.html index 5e39d02da..957ed590e 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1,4 +1,4 @@ -php-wasm