mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat(node,python): opt-in Playwright browser installation (#653)
* feat: automatically install packages required for playwright, and playwright browser This reverts commit 2a5567ff09fa044cf391b1d79ecf1fe7846e8828. * docs: update environment variable test configuration property name Generated-by: aiautocommit * docs(node): add production deployment guide link to start command help Generated-by: aiautocommit * feat(python-playwright): install playwright only when requested via env - Enable Playwright browser installation conditionally using `RAILPACK_PYTHON_PLAYWRIGHT_INSTALL`. - Update python-playwright example to test against example.com. Generated-by: aiautocommit * feat(python): make Playwright browser installation opt-in - Require `RAILPACK_PYTHON_PLAYWRIGHT_INSTALL=1` to install Playwright browsers and their runtime dependencies automatically. - Prevent unintended deployment size increases and breaking changes for existing users since automatic installation can cause issues. - Add test coverage verifying opt-in behavior and production dependency resolution. Generated-by: aiautocommit * docs(python): document playwright install configuration variable Generated-by: aiautocommit * feat(node): add support for checking production-only dependencies Generated-by: aiautocommit * feat(node): make Playwright browser installation opt-in - Restrict automatic Playwright detection to production dependencies and require an explicit environment variable to install browsers, preventing unexpected side effects for existing users. - Add tests to verify Playwright installation behavior based on environment configuration. Generated-by: aiautocommit * docs: add unit testing guideline against testing log output Generated-by: aiautocommit * chore(deps): update playwright dependency to v1.60 in node-playwright example Generated-by: aiautocommit * feat(node,python): update Playwright runtime dependencies for Debian 12 - Align Playwright runtime dependencies with the official nativeDeps.ts list for Debian 12 Chromium. - Ensure production runtime images include all required system libraries for Playwright without relying on builder-only installation flags. Generated-by: aiautocommit * feat(examples): navigate to example.com in playwright node example Generated-by: aiautocommit * docs(node): document opt-in Playwright browser installation Generated-by: aiautocommit * test(core): update test snapshots for playwright production-only dependencies Generated-by: aiautocommit * docs: add instruction regarding snapshot updates to PR template Generated-by: aiautocommit * feat(node): remove libatomic1 requirement for Node.js 25+ Generated-by: aiautocommit * test(core): update playwright snapshots for mise version and apt caches Generated-by: aiautocommit
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
|
||||
## Test
|
||||
|
||||
<!-- how is this change tested? -->
|
||||
<!-- how is this change tested? Do not indicate that snapshots were updated -->
|
||||
|
||||
## Links
|
||||
|
||||
<!-- link(s) to any relevant external documentation, github issues this fixes or is connected to , etc-->
|
||||
<!-- link(s) to any relevant external documentation, github issues this fixes or is connected to , etc-->
|
||||
|
||||
@@ -72,7 +72,11 @@ There are normal unit tests, snapshot tests, and integration tests. The integrat
|
||||
* In `test.json` we should avoid using `justBuild` for all but the most simple projects. `justBuild` does not test `expectedOutput` or any other assertions.
|
||||
* If the project has a server component, we should use a `httpCheck` test. Read the @docs/src/content/docs/guides/developing-locally.md guide, specifically the `### HTTP Checks` section for more information.
|
||||
* `httpCheck` assertions assume that `$PORT` is respected.
|
||||
* You can use `"env": { "SECRET": "123"}` to add a required environment variable to a test case.
|
||||
* You can use `"envs": { "SECRET": "123"}` to add a required environment variable to a test case.
|
||||
|
||||
## Unit tests
|
||||
|
||||
* Do not test log output.
|
||||
|
||||
# File Conventions
|
||||
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"caches": {
|
||||
"apt": {
|
||||
"directory": "/var/cache/apt",
|
||||
"type": "locked"
|
||||
},
|
||||
"apt-lists": {
|
||||
"directory": "/var/lib/apt/lists",
|
||||
"type": "locked"
|
||||
},
|
||||
"node-modules": {
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"pnpm-install": {
|
||||
"directory": "/opt/pnpm/store",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"step": "packages:apt:runtime"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/node_modules"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn"
|
||||
],
|
||||
"include": [
|
||||
"/root/.cache",
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "node index.js",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"RAILPACK_VERSION": "dev"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"dest": "mise.toml",
|
||||
"src": "mise.toml"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: node, pnpm"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.15"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"pnpm-install"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package.json",
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"dest": "pnpm-lock.yaml",
|
||||
"src": "pnpm-lock.yaml"
|
||||
},
|
||||
{
|
||||
"path": "/opt/pnpm/bin"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm add -g node-gyp"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm install --frozen-lockfile --prefer-offline"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"PNPM_HOME": "/opt/pnpm",
|
||||
"PNPM_STORE_DIR": "/opt/pnpm/store"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"node-modules"
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"apt",
|
||||
"apt-lists"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
|
||||
"customName": "install apt packages: libatomic1"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.7.15"
|
||||
}
|
||||
],
|
||||
"name": "packages:apt:runtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"caches": {
|
||||
"uv": {
|
||||
"directory": "/opt/uv-cache",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.7.15"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/.venv"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
".venv"
|
||||
],
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "python main.py",
|
||||
"variables": {
|
||||
"PIP_DEFAULT_TIMEOUT": "100",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONFAULTHANDLER": "1",
|
||||
"PYTHONHASHSEED": "random",
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"RAILPACK_VERSION": "dev"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"dest": "mise.toml",
|
||||
"src": "mise.toml"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: python, uv"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.15"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"uv"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"dest": "pyproject.toml",
|
||||
"src": "pyproject.toml"
|
||||
},
|
||||
{
|
||||
"dest": "uv.lock",
|
||||
"src": "uv.lock"
|
||||
},
|
||||
{
|
||||
"path": "/root/.local/bin"
|
||||
},
|
||||
{
|
||||
"path": "/app/.venv/bin"
|
||||
},
|
||||
{
|
||||
"cmd": "uv sync --locked --no-dev --no-install-project"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"PIP_DEFAULT_TIMEOUT": "100",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONFAULTHANDLER": "1",
|
||||
"PYTHONHASHSEED": "random",
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"UV_CACHE_DIR": "/opt/uv-cache",
|
||||
"UV_COMPILE_BYTECODE": "1",
|
||||
"UV_LINK_MODE": "copy",
|
||||
"UV_PYTHON_DOWNLOADS": "never",
|
||||
"VIRTUAL_ENV": "/app/.venv"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "uv sync --locked --no-dev --no-editable"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,12 +20,45 @@ const (
|
||||
DEFAULT_NODE_VERSION = "22"
|
||||
DEFAULT_BUN_VERSION = "latest"
|
||||
|
||||
COREPACK_HOME = "/opt/corepack"
|
||||
|
||||
// not used by npm, but many other tools: next, jest, webpack, etc
|
||||
NODE_MODULES_CACHE = "/app/node_modules/.cache"
|
||||
NODE_MODULES_CACHE = "/app/node_modules/.cache"
|
||||
COREPACK_HOME = "/opt/corepack"
|
||||
PLAYWRIGHT_CACHE_DIR = "/root/.cache/ms-playwright"
|
||||
PLAYWRIGHT_INSTALL_VAR = "NODE_PLAYWRIGHT_INSTALL"
|
||||
)
|
||||
|
||||
var nodeRuntimeDepRequirements = map[string][]string{
|
||||
// To find the latest list: run `npx puppeteer@latest install --help` and inspect docs
|
||||
"puppeteer": {"xvfb", "gconf-service", "libasound2", "libatk1.0-0", "libc6", "libcairo2", "libcups2", "libdbus-1-3", "libexpat1", "libfontconfig1", "libgbm1", "libgcc1", "libgconf-2-4", "libgdk-pixbuf2.0-0", "libglib2.0-0", "libgtk-3-0", "libnspr4", "libpango-1.0-0", "libpangocairo-1.0-0", "libstdc++6", "libx11-6", "libx11-xcb1", "libxcb1", "libxcomposite1", "libxcursor1", "libxdamage1", "libxext6", "libxfixes3", "libxi6", "libxrandr2", "libxrender1", "libxss1", "libxtst6", "ca-certificates", "fonts-liberation", "libappindicator1", "libnss3", "lsb-release", "xdg-utils", "wget"},
|
||||
}
|
||||
|
||||
// Keep this aligned with the Debian 12 Chromium list in Playwright's nativeDeps.ts:
|
||||
// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/nativeDeps.ts
|
||||
// Keep these explicit because --with-deps installs them in the builder, not the runtime image.
|
||||
var nodePlaywrightRuntimeDependencies = []string{
|
||||
"libasound2",
|
||||
"libatk-bridge2.0-0",
|
||||
"libatk1.0-0",
|
||||
"libatspi2.0-0",
|
||||
"libcairo2",
|
||||
"libcups2",
|
||||
"libdbus-1-3",
|
||||
"libdrm2",
|
||||
"libgbm1",
|
||||
"libglib2.0-0",
|
||||
"libnspr4",
|
||||
"libnss3",
|
||||
"libpango-1.0-0",
|
||||
"libx11-6",
|
||||
"libxcb1",
|
||||
"libxcomposite1",
|
||||
"libxdamage1",
|
||||
"libxext6",
|
||||
"libxfixes3",
|
||||
"libxkbcommon0",
|
||||
"libxrandr2",
|
||||
}
|
||||
|
||||
var (
|
||||
// bunCommandRegex matches "bun" or "bunx" as a command (not part of another word)
|
||||
bunCommandRegex = regexp.MustCompile(`(^|\s|;|&|&&|\||\|\|)bunx?\s`)
|
||||
@@ -143,9 +176,29 @@ func (p *NodeProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
}
|
||||
|
||||
runtimeAptPackages := []string{}
|
||||
|
||||
if p.usesPuppeteer() {
|
||||
ctx.Logger.LogInfo("Installing puppeteer dependencies")
|
||||
runtimeAptPackages = append(runtimeAptPackages, "xvfb", "gconf-service", "libasound2", "libatk1.0-0", "libc6", "libcairo2", "libcups2", "libdbus-1-3", "libexpat1", "libfontconfig1", "libgbm1", "libgcc1", "libgconf-2-4", "libgdk-pixbuf2.0-0", "libglib2.0-0", "libgtk-3-0", "libnspr4", "libpango-1.0-0", "libpangocairo-1.0-0", "libstdc++6", "libx11-6", "libx11-xcb1", "libxcb1", "libxcomposite1", "libxcursor1", "libxdamage1", "libxext6", "libxfixes3", "libxi6", "libxrandr2", "libxrender1", "libxss1", "libxtst6", "ca-certificates", "fonts-liberation", "libappindicator1", "libnss3", "lsb-release", "xdg-utils", "wget")
|
||||
ctx.Logger.LogInfo("Installing puppeteer packages")
|
||||
runtimeAptPackages = append(runtimeAptPackages, nodeRuntimeDepRequirements["puppeteer"]...)
|
||||
}
|
||||
|
||||
usesPlaywright := p.usesProductionPlaywright()
|
||||
installPlaywright := ctx.Env.IsConfigVariableTruthy(PLAYWRIGHT_INSTALL_VAR)
|
||||
|
||||
// Automatic browser installation caused issues for an existing user, so keep this opt-in.
|
||||
if usesPlaywright && !installPlaywright {
|
||||
ctx.Logger.LogSuggestion(
|
||||
"Set `RAILPACK_NODE_PLAYWRIGHT_INSTALL=1` to install Playwright browsers",
|
||||
"/languages/node#playwright",
|
||||
)
|
||||
}
|
||||
|
||||
if installPlaywright {
|
||||
ctx.Logger.LogInfo("Installing playwright packages and headless browser")
|
||||
runtimeAptPackages = append(runtimeAptPackages, nodePlaywrightRuntimeDependencies...)
|
||||
// --only-shell installs the smaller Chromium headless shell, which is
|
||||
// more appropriate for server environments than full Chromium.
|
||||
install.AddCommand(plan.NewExecCommand(p.packageManager.ExecCommand("playwright install --only-shell")))
|
||||
}
|
||||
|
||||
nodeModulesLayer := plan.NewStepLayer(build.Name(), plan.Filter{
|
||||
@@ -185,7 +238,8 @@ func (p *NodeProvider) StartCommandHelp() string {
|
||||
"4. An Nx workspace with a Next.js app (nx.json + next)\n\n" +
|
||||
"If you have a static site, you can set the RAILPACK_SPA_OUTPUT_DIR environment variable\n" +
|
||||
"containing the directory of your built static files.\n\n" +
|
||||
"For multi-app Nx workspaces, set RAILPACK_NX_APP to select which app to deploy."
|
||||
"For multi-app Nx workspaces, set RAILPACK_NX_APP to select which app to deploy.\n\n" +
|
||||
"For more information, see the Node.js production deployment guide: https://railway.app/docs/deploy/node"
|
||||
}
|
||||
|
||||
func (p *NodeProvider) GetStartCommand(ctx *generate.GenerateContext) string {
|
||||
@@ -450,6 +504,10 @@ func (p *NodeProvider) usesPuppeteer() bool {
|
||||
return p.workspace.HasDependency("puppeteer")
|
||||
}
|
||||
|
||||
func (p *NodeProvider) usesProductionPlaywright() bool {
|
||||
return p.workspace.HasProductionDependency("playwright")
|
||||
}
|
||||
|
||||
// determine the major version of yarn from a version string. These major versions are installed and managed quite
|
||||
// differently which is why we need to distinguish them here.
|
||||
func parseYarnPackageManager(pmVersion string) PackageManager {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/railwayapp/railpack/core/app"
|
||||
"github.com/railwayapp/railpack/core/generate"
|
||||
"github.com/railwayapp/railpack/core/plan"
|
||||
testingUtils "github.com/railwayapp/railpack/core/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -315,3 +316,59 @@ func TestUsesPnpmBinSubdir(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaywrightInstallationIsOptIn(t *testing.T) {
|
||||
t.Run("does not install by default", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/node-playwright")
|
||||
provider := NodeProvider{}
|
||||
|
||||
require.NoError(t, provider.Initialize(ctx))
|
||||
require.NoError(t, provider.Plan(ctx))
|
||||
require.False(t, nodeStepHasExecCommand(
|
||||
ctx,
|
||||
"install",
|
||||
"pnpm exec playwright install --only-shell",
|
||||
))
|
||||
require.NotContains(t, ctx.Deploy.AptPackages, "libnss3")
|
||||
})
|
||||
|
||||
t.Run("installs when enabled", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/node-playwright")
|
||||
ctx.Env.Variables["RAILPACK_NODE_PLAYWRIGHT_INSTALL"] = "1"
|
||||
provider := NodeProvider{}
|
||||
|
||||
require.NoError(t, provider.Initialize(ctx))
|
||||
require.NoError(t, provider.Plan(ctx))
|
||||
require.True(t, nodeStepHasExecCommand(
|
||||
ctx,
|
||||
"install",
|
||||
"pnpm exec playwright install --only-shell",
|
||||
))
|
||||
require.Contains(t, ctx.Deploy.AptPackages, "libnss3")
|
||||
})
|
||||
}
|
||||
|
||||
func nodeStepHasExecCommand(
|
||||
ctx *generate.GenerateContext,
|
||||
stepName string,
|
||||
command string,
|
||||
) bool {
|
||||
step := ctx.GetStepByName(stepName)
|
||||
if step == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
commandStep, ok := (*step).(*generate.CommandStepBuilder)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, candidate := range commandStep.Commands {
|
||||
execCommand, ok := candidate.(plan.ExecCommand)
|
||||
if ok && execCommand.Cmd == command {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -47,10 +47,8 @@ func (p *PackageJson) BuildScriptContains(value string) bool {
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasDependency(dependency string) bool {
|
||||
if p.Dependencies != nil {
|
||||
if _, ok := p.Dependencies[dependency]; ok {
|
||||
return true
|
||||
}
|
||||
if p.hasProductionDependency(dependency) {
|
||||
return true
|
||||
}
|
||||
|
||||
if p.DevDependencies != nil {
|
||||
@@ -62,6 +60,15 @@ func (p *PackageJson) hasDependency(dependency string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasProductionDependency(dependency string) bool {
|
||||
if p.Dependencies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := p.Dependencies[dependency]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *PackageJson) hasLocalDependency() bool {
|
||||
allDeps := make(map[string]string)
|
||||
maps.Copy(allDeps, p.Dependencies)
|
||||
|
||||
@@ -71,6 +71,17 @@ func (p PackageManager) misePackageName() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (p PackageManager) ExecCommand(cmd string) string {
|
||||
switch p {
|
||||
case PackageManagerPnpm:
|
||||
return fmt.Sprintf("pnpm exec %s", cmd)
|
||||
case PackageManagerBun:
|
||||
return fmt.Sprintf("bunx %s", cmd)
|
||||
default:
|
||||
return fmt.Sprintf("npx %s", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (p PackageManager) installDependencies(ctx *generate.GenerateContext, workspace *Workspace, install *generate.CommandStepBuilder, usingCorepack bool) {
|
||||
packageJsons := workspace.AllPackageJson()
|
||||
|
||||
|
||||
@@ -140,6 +140,20 @@ func (w *Workspace) HasDependency(dependency string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Workspace) HasProductionDependency(dependency string) bool {
|
||||
if w.Root.PackageJson.hasProductionDependency(dependency) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, pkg := range w.Packages {
|
||||
if pkg.PackageJson.hasProductionDependency(dependency) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Workspace) AllPackageJson() []*PackageJson {
|
||||
packageJsons := []*PackageJson{w.Root.PackageJson}
|
||||
|
||||
|
||||
@@ -158,3 +158,41 @@ func TestWorkspaceAllPackageJson(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceHasProductionDependency(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
dependency string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "root production dependency",
|
||||
path: "../../../examples/node-playwright",
|
||||
dependency: "playwright",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "root development dependency",
|
||||
path: "../../../examples/node-npm",
|
||||
dependency: "typescript",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "workspace production dependency",
|
||||
path: "../../../examples/node-npm-workspaces",
|
||||
dependency: "express",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, tt.path)
|
||||
workspace, err := NewWorkspace(ctx.App)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.want, workspace.HasProductionDependency(tt.dependency))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package python
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/railpack/core/generate"
|
||||
)
|
||||
|
||||
var (
|
||||
// Extract the leading distribution name from requirements.txt and PEP 508 dependency specs.
|
||||
// This is not used for Poetry or Pipenv tables, where the distribution name is the TOML key.
|
||||
pythonDependencyNameRegex = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*`)
|
||||
|
||||
// Normalize the interchangeable package-name separators defined by Python packaging conventions.
|
||||
pythonDependencyNameSeparatorRegex = regexp.MustCompile(`[-_.]+`)
|
||||
)
|
||||
|
||||
type pyprojectManifest struct {
|
||||
Project struct {
|
||||
Dependencies []string `toml:"dependencies"`
|
||||
} `toml:"project"`
|
||||
Tool struct {
|
||||
Poetry struct {
|
||||
Dependencies map[string]any `toml:"dependencies"`
|
||||
} `toml:"poetry"`
|
||||
} `toml:"tool"`
|
||||
}
|
||||
|
||||
type pipfileManifest struct {
|
||||
Packages map[string]any `toml:"packages"`
|
||||
}
|
||||
|
||||
func (p *PythonProvider) usesProductionDep(ctx *generate.GenerateContext, dep string) bool {
|
||||
if p.hasRequirements(ctx) {
|
||||
return requirementsUseDependency(ctx, dep)
|
||||
}
|
||||
|
||||
if p.hasPyproject(ctx) {
|
||||
return pyprojectsUseProductionDependency(ctx, dep)
|
||||
}
|
||||
|
||||
if p.hasPipfile(ctx) {
|
||||
return pipfileUsesProductionDependency(ctx, dep)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func requirementsUseDependency(ctx *generate.GenerateContext, dep string) bool {
|
||||
contents, err := ctx.App.ReadFile("requirements.txt")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for line := range strings.Lines(contents) {
|
||||
if dependencySpecMatches(line, dep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func pyprojectsUseProductionDependency(ctx *generate.GenerateContext, dep string) bool {
|
||||
files, err := ctx.App.FindFiles("**/pyproject.toml")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
var manifest pyprojectManifest
|
||||
if err := ctx.App.ReadTOML(file, &manifest); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, dependency := range manifest.Project.Dependencies {
|
||||
if dependencySpecMatches(dependency, dep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for dependency := range manifest.Tool.Poetry.Dependencies {
|
||||
if normalizedDependencyName(dependency) == normalizedDependencyName(dep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func pipfileUsesProductionDependency(ctx *generate.GenerateContext, dep string) bool {
|
||||
var manifest pipfileManifest
|
||||
if err := ctx.App.ReadTOML("Pipfile", &manifest); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for dependency := range manifest.Packages {
|
||||
if normalizedDependencyName(dependency) == normalizedDependencyName(dep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func dependencySpecMatches(spec string, dep string) bool {
|
||||
name := pythonDependencyNameRegex.FindString(strings.TrimSpace(spec))
|
||||
return normalizedDependencyName(name) == normalizedDependencyName(dep)
|
||||
}
|
||||
|
||||
func normalizedDependencyName(name string) string {
|
||||
return pythonDependencyNameSeparatorRegex.ReplaceAllString(strings.ToLower(name), "-")
|
||||
}
|
||||
@@ -18,8 +18,36 @@ const (
|
||||
PIP_CACHE_DIR = "/opt/pip-cache"
|
||||
VENV_PATH = "/app/.venv"
|
||||
LOCAL_BIN_PATH = "/root/.local/bin"
|
||||
PLAYWRIGHT_CACHE_DIR = "/root/.cache/ms-playwright"
|
||||
PLAYWRIGHT_INSTALL_VAR = "PYTHON_PLAYWRIGHT_INSTALL"
|
||||
)
|
||||
|
||||
// Keep this aligned with the Debian 12 Chromium list in Playwright's nativeDeps.ts:
|
||||
// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/nativeDeps.ts
|
||||
var pythonPlaywrightRuntimeDependencies = []string{
|
||||
"libasound2",
|
||||
"libatk-bridge2.0-0",
|
||||
"libatk1.0-0",
|
||||
"libatspi2.0-0",
|
||||
"libcairo2",
|
||||
"libcups2",
|
||||
"libdbus-1-3",
|
||||
"libdrm2",
|
||||
"libgbm1",
|
||||
"libglib2.0-0",
|
||||
"libnspr4",
|
||||
"libnss3",
|
||||
"libpango-1.0-0",
|
||||
"libx11-6",
|
||||
"libxcb1",
|
||||
"libxcomposite1",
|
||||
"libxdamage1",
|
||||
"libxext6",
|
||||
"libxfixes3",
|
||||
"libxkbcommon0",
|
||||
"libxrandr2",
|
||||
}
|
||||
|
||||
type PythonProvider struct{}
|
||||
|
||||
func (p *PythonProvider) Name() string {
|
||||
@@ -67,6 +95,26 @@ func (p *PythonProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
installOutputs = p.InstallPipenv(ctx, install)
|
||||
}
|
||||
|
||||
usesPlaywright := p.usesProductionDep(ctx, "playwright")
|
||||
installPlaywright := ctx.Env.IsConfigVariableTruthy(PLAYWRIGHT_INSTALL_VAR)
|
||||
|
||||
// Automatic browser installation caused issues for an existing user, so keep this opt-in.
|
||||
if usesPlaywright && !installPlaywright {
|
||||
ctx.Logger.LogSuggestion(
|
||||
"Set `RAILPACK_PYTHON_PLAYWRIGHT_INSTALL=1` to install Playwright browsers",
|
||||
"/languages/python#playwright",
|
||||
)
|
||||
}
|
||||
|
||||
if installPlaywright {
|
||||
ctx.Logger.LogInfo("Installing Playwright chromium browser")
|
||||
// --only-shell installs the smaller Chromium headless shell, which is
|
||||
// more appropriate for server environments than full Chromium.
|
||||
install.AddCommand(plan.NewExecCommand("playwright install --only-shell"))
|
||||
// Include the browser cache so its binaries are available in the deploy stage.
|
||||
installOutputs = append(installOutputs, PLAYWRIGHT_CACHE_DIR)
|
||||
}
|
||||
|
||||
p.addMetadata(ctx)
|
||||
|
||||
build.AddInput(plan.NewStepLayer(install.Name()))
|
||||
@@ -157,14 +205,16 @@ func (p *PythonProvider) InstallUv(ctx *generate.GenerateContext, install *gener
|
||||
install.AddEnvVars(p.GetPythonEnvVars(ctx))
|
||||
|
||||
p.copyInstallFiles(ctx, install)
|
||||
install.AddCommands([]plan.Command{
|
||||
installCommands := []plan.Command{
|
||||
plan.NewPathCommand(LOCAL_BIN_PATH),
|
||||
plan.NewPathCommand(VENV_PATH + "/bin"),
|
||||
// if we exclude workspace packages, uv.lock will fail the frozen test and the user will get an error
|
||||
// to avoid this, we (a) detect if workspace packages are required (b) if they aren't, we don't include project
|
||||
// source in order to optimize layer caching (c) install project in the build phase.
|
||||
plan.NewExecCommand("uv sync --locked --no-dev --no-install-project"),
|
||||
})
|
||||
}
|
||||
|
||||
install.AddCommands(installCommands)
|
||||
|
||||
return []string{VENV_PATH}
|
||||
}
|
||||
@@ -209,11 +259,13 @@ func (p *PythonProvider) InstallPDM(ctx *generate.GenerateContext, install *gene
|
||||
})
|
||||
|
||||
p.copyInstallFiles(ctx, install)
|
||||
install.AddCommands([]plan.Command{
|
||||
installCommands := []plan.Command{
|
||||
plan.NewPathCommand(LOCAL_BIN_PATH),
|
||||
plan.NewPathCommand(VENV_PATH + "/bin"),
|
||||
plan.NewExecCommand("pdm install --check --prod --no-editable"),
|
||||
})
|
||||
}
|
||||
|
||||
install.AddCommands(installCommands)
|
||||
|
||||
return []string{VENV_PATH}
|
||||
}
|
||||
@@ -229,11 +281,13 @@ func (p *PythonProvider) InstallPoetry(ctx *generate.GenerateContext, install *g
|
||||
})
|
||||
|
||||
p.copyInstallFiles(ctx, install)
|
||||
install.AddCommands([]plan.Command{
|
||||
installCommands := []plan.Command{
|
||||
plan.NewPathCommand(LOCAL_BIN_PATH),
|
||||
plan.NewPathCommand(VENV_PATH + "/bin"),
|
||||
plan.NewExecCommand("poetry install --no-interaction --no-ansi --only main --no-root"),
|
||||
})
|
||||
}
|
||||
|
||||
install.AddCommands(installCommands)
|
||||
|
||||
return []string{VENV_PATH}
|
||||
}
|
||||
@@ -268,6 +322,11 @@ func (p *PythonProvider) AddRuntimeDeps(ctx *generate.GenerateContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Env.IsConfigVariableTruthy(PLAYWRIGHT_INSTALL_VAR) {
|
||||
ctx.Logger.LogInfo("Installing runtime apt packages for playwright: %v", pythonPlaywrightRuntimeDependencies)
|
||||
ctx.Deploy.AddAptPackages(pythonPlaywrightRuntimeDependencies)
|
||||
}
|
||||
|
||||
if p.usesPostgres(ctx) {
|
||||
ctx.Deploy.AddAptPackages([]string{"libpq5"})
|
||||
}
|
||||
@@ -468,6 +527,7 @@ func (p *PythonProvider) addMetadata(ctx *generate.GenerateContext) {
|
||||
ctx.Metadata.Set("pythonRuntime", p.getRuntime(ctx))
|
||||
}
|
||||
|
||||
// TODO this is incredibly naive: we should parse the files we can distinguish between prod and dev
|
||||
func (p *PythonProvider) usesDep(ctx *generate.GenerateContext, dep string) bool {
|
||||
files, err := ctx.App.FindFiles("**/{requirements.txt,pyproject.toml,Pipfile}")
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,8 @@ package python
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/railwayapp/railpack/core/generate"
|
||||
"github.com/railwayapp/railpack/core/plan"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
testingUtils "github.com/railwayapp/railpack/core/testing"
|
||||
@@ -142,3 +144,95 @@ func TestUsesPostgres(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsesProductionPlaywrightDependency(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
dependency string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "PEP 621 production dependency",
|
||||
path: "../../../examples/python-playwright",
|
||||
dependency: "playwright",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// python-playwright declares pytest in dependency-groups.dev.
|
||||
name: "PEP 735 development dependency",
|
||||
path: "../../../examples/python-playwright",
|
||||
dependency: "pytest",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Poetry production dependency",
|
||||
path: "../../../examples/python-poetry",
|
||||
dependency: "flask",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Pipenv production dependency",
|
||||
path: "../../../examples/python-pipfile",
|
||||
dependency: "cowsay",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, tt.path)
|
||||
provider := PythonProvider{}
|
||||
|
||||
require.Equal(t, tt.want, provider.usesProductionDep(ctx, tt.dependency))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaywrightInstallationIsOptIn(t *testing.T) {
|
||||
t.Run("does not install by default", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/python-playwright")
|
||||
provider := PythonProvider{}
|
||||
|
||||
require.NoError(t, provider.Plan(ctx))
|
||||
require.False(t, stepHasExecCommand(ctx, "install", "playwright install --only-shell"))
|
||||
require.NotContains(t, ctx.Deploy.AptPackages, "libnss3")
|
||||
})
|
||||
|
||||
t.Run("install when enabled", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, "../../../examples/python-playwright")
|
||||
ctx.Env.Variables["RAILPACK_PYTHON_PLAYWRIGHT_INSTALL"] = "1"
|
||||
provider := PythonProvider{}
|
||||
|
||||
require.NoError(t, provider.Plan(ctx))
|
||||
require.True(t, stepHasExecCommand(ctx, "install", "playwright install --only-shell"))
|
||||
require.Contains(t, ctx.Deploy.AptPackages, "libnss3")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDependencySpecMatches(t *testing.T) {
|
||||
require.True(t, dependencySpecMatches("Playwright[chromium]>=1.49", "playwright"))
|
||||
require.True(t, dependencySpecMatches("playwright @ https://example.com/playwright.whl", "playwright"))
|
||||
require.False(t, dependencySpecMatches("pytest-playwright>=0.7", "playwright"))
|
||||
}
|
||||
|
||||
func stepHasExecCommand(ctx *generate.GenerateContext, stepName string, command string) bool {
|
||||
step := ctx.GetStepByName(stepName)
|
||||
if step == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
commandStep, ok := (*step).(*generate.CommandStepBuilder)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, candidate := range commandStep.Commands {
|
||||
execCommand, ok := candidate.(plan.ExecCommand)
|
||||
if ok && execCommand.Cmd == command {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ func validateStartCommand(plan *plan.BuildPlan, logger *logger.Logger, options *
|
||||
return true
|
||||
}
|
||||
|
||||
msg := "No start command detected. Specify a start command: https://railpack.com/config/file"
|
||||
msg := "No start command detected. Specify a start command: https://railpack.com/config/file#deploy"
|
||||
if options.ProviderToUse != nil {
|
||||
if providerHelp := options.ProviderToUse.StartCommandHelp(); providerHelp != "" {
|
||||
msg += "\n\n" + providerHelp
|
||||
|
||||
@@ -97,6 +97,19 @@ Railpack determines the start command in the following order:
|
||||
| `RAILPACK_NODE_INSTALL_PATTERNS` | Custom patterns to install dependencies | `prisma` |
|
||||
| `RAILPACK_ANGULAR_PROJECT` | Name of the Angular project to build | `my-app` |
|
||||
| `RAILPACK_NX_APP` | Nx app to build and start (project name, package name, or path) | `web` or `@org/web` |
|
||||
| `RAILPACK_NODE_PLAYWRIGHT_INSTALL` | Install Playwright browsers | `1` |
|
||||
|
||||
### Playwright
|
||||
|
||||
When Playwright is a production dependency, Railpack suggests setting
|
||||
`RAILPACK_NODE_PLAYWRIGHT_INSTALL=1`. Browser installation is opt-in because
|
||||
it increases image size and is not required by every application that includes
|
||||
Playwright.
|
||||
|
||||
When enabled, Railpack runs Playwright through the detected package manager to
|
||||
install its browser binaries and adds the required runtime system packages.
|
||||
Ensure Playwright is included in your production dependencies so its CLI is
|
||||
available during the build.
|
||||
|
||||
### Package Managers
|
||||
|
||||
@@ -270,7 +283,7 @@ commands used.
|
||||
|
||||
### System Dependencies
|
||||
|
||||
Railpack automatically installs system dependencies for certain packages:
|
||||
Railpack automatically installs system dependencies for Puppeteer:
|
||||
|
||||
- **Puppeteer**: When detected in workspace dependencies, Railpack installs
|
||||
all necessary system packages for running headless Chrome, including
|
||||
@@ -278,7 +291,5 @@ Railpack automatically installs system dependencies for certain packages:
|
||||
Puppeteer's bundled Chromium [does not support
|
||||
ARM64](https://github.com/puppeteer/puppeteer/issues/7740); if you need
|
||||
to run on ARM hardware, consider switching to
|
||||
[Playwright](#system-dependencies) or implementing a custom workaround
|
||||
[Playwright](#playwright) or implementing a custom workaround
|
||||
(e.g. installing a system Chromium and pointing `executablePath` at it).
|
||||
- **Playwright**: When detected in workspace dependencies, Railpack installs
|
||||
the necessary system packages and the headless shell version of Chromium
|
||||
|
||||
@@ -102,10 +102,22 @@ Railpack supports multiple Python package managers:
|
||||
|
||||
### Config Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
| -------------------------- | --------------------------- | ------------ |
|
||||
| `RAILPACK_PYTHON_VERSION` | Override the Python version | `3.11` |
|
||||
| `RAILPACK_DJANGO_APP_NAME` | Django app name | `myapp.wsgi` |
|
||||
| Variable | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `RAILPACK_PYTHON_VERSION` | Override the Python version | `3.11` |
|
||||
| `RAILPACK_DJANGO_APP_NAME` | Django app name | `myapp.wsgi` |
|
||||
| `RAILPACK_PYTHON_PLAYWRIGHT_INSTALL` | Install browser binaries | `1` |
|
||||
|
||||
### Playwright
|
||||
|
||||
When Playwright is a production dependency, Railpack suggests setting
|
||||
`RAILPACK_PYTHON_PLAYWRIGHT_INSTALL=1`. Railpack does not install browser
|
||||
binaries automatically because doing so can unexpectedly change existing
|
||||
builds and increase image size.
|
||||
|
||||
When enabled, Railpack installs Playwright's browser binaries and the system
|
||||
packages needed to run them. Ensure Playwright is included in your production
|
||||
dependencies so its CLI is available during the build.
|
||||
|
||||
### System Dependencies
|
||||
|
||||
@@ -115,8 +127,6 @@ Railpack installs system dependencies for common Python packages:
|
||||
- **pdf2image**: Installs `poppler-utils`
|
||||
- **pydub**: Installs `ffmpeg`
|
||||
- **pymovie**: Installs `ffmpeg`, `qt5-qmake`, and related Qt packages
|
||||
- **Playwright**: When detected in dependencies, Railpack installs the necessary
|
||||
system packages and the headless shell version of Chromium
|
||||
|
||||
## Framework Support
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
(async () => {
|
||||
console.log("Starting Playwright");
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox"],
|
||||
});
|
||||
const version = browser.version();
|
||||
console.log(`Chromium version: ${version}`);
|
||||
console.log("Creating Page");
|
||||
const page = await browser.newPage();
|
||||
console.log("Navigating to example.com");
|
||||
await page.goto("https://example.com", {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log("Hello from playwright");
|
||||
})();
|
||||
@@ -0,0 +1,3 @@
|
||||
[tools]
|
||||
node = "latest"
|
||||
pnpm = "latest"
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "playwright-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"playwright": "^1.60"
|
||||
}
|
||||
}
|
||||
Generated
+43
@@ -0,0 +1,43 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
playwright:
|
||||
specifier: ^1.60
|
||||
version: 1.62.0
|
||||
|
||||
packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
playwright-core: 1.62.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"envs": {
|
||||
"RAILPACK_NODE_PLAYWRIGHT_INSTALL": "1"
|
||||
},
|
||||
"expectedOutput": "Hello from playwright"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
print("Starting Playwright")
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
version = browser.version
|
||||
print(f"Chromium version: {version}")
|
||||
print("Creating Page")
|
||||
page = browser.new_page()
|
||||
print("Navigating to example.com")
|
||||
page.goto("https://example.com", wait_until="networkidle")
|
||||
assert page.title() == "Example Domain"
|
||||
browser.close()
|
||||
|
||||
print("Hello from playwright")
|
||||
@@ -0,0 +1,3 @@
|
||||
[tools]
|
||||
python = "latest"
|
||||
uv = "latest"
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "python-playwright"
|
||||
version = "0.1.0"
|
||||
description = "Python Playwright example"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"playwright>=1.49",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"envs": {
|
||||
"RAILPACK_PYTHON_PLAYWRIGHT_INSTALL": "1"
|
||||
},
|
||||
"expectedOutput": "Hello from playwright"
|
||||
}
|
||||
]
|
||||
Generated
+173
@@ -0,0 +1,173 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "1.56.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "pyee" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/31/a5362cee43f844509f1f10d8a27c9cc0e2f7bdce5353d304d93b2151c1b1/playwright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33eb89c516cbc6723f2e3523bada4a4eb0984a9c411325c02d7016a5d625e9c", size = 40611424, upload-time = "2025-11-11T18:39:10.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/95/347eef596d8778fb53590dc326c344d427fa19ba3d42b646fce2a4572eb3/playwright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b228b3395212b9472a4ee5f1afe40d376eef9568eb039fcb3e563de8f4f4657b", size = 39400228, upload-time = "2025-11-11T18:39:13.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/54/6ad97b08b2ca1dfcb4fbde4536c4f45c0d9d8b1857a2d20e7bbfdf43bf15/playwright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0ef7e6fd653267798a8a968ff7aa2dcac14398b7dd7440ef57524e01e0fbbd65", size = 40611424, upload-time = "2025-11-11T18:39:17.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/76/6d409e37e82cdd5dda3df1ab958130ae32b46e42458bd4fc93d7eb8749cb/playwright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:404be089b49d94bc4c1fe0dfb07664bda5ffe87789034a03bffb884489bdfb5c", size = 46263122, upload-time = "2025-11-11T18:39:20.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/84/fb292cc5d45f3252e255ea39066cd1d2385c61c6c1596548dfbf59c88605/playwright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cda7cf4e51c0d35dab55190841bfcdfb5871685ec22cb722cd0ad2df183e34", size = 46110645, upload-time = "2025-11-11T18:39:24.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/bd/8c02c3388ae14edc374ac9f22cbe4e14826c6a51b2d8eaf86e89fabee264/playwright-1.56.0-py3-none-win32.whl", hash = "sha256:d87b79bcb082092d916a332c27ec9732e0418c319755d235d93cc6be13bdd721", size = 35639837, upload-time = "2025-11-11T18:39:27.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/27/f13b538fbc6b7a00152f4379054a49f6abc0bf55ac86f677ae54bc49fb82/playwright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:3c7fc49bb9e673489bf2622855f9486d41c5101bbed964638552b864c4591f94", size = 35639843, upload-time = "2025-11-11T18:39:30.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c7/3ee8b556107995846576b4fe42a08ed49b8677619421f2afacf6ee421138/playwright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:2745490ae8dd58d27e5ea4d9aa28402e8e2991eb84fb4b2fd5fbde2106716f6f", size = 31248959, upload-time = "2025-11-11T18:39:33.998Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyee"
|
||||
version = "13.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-playwright"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "playwright" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "playwright", specifier = ">=1.49" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
@@ -70,6 +70,10 @@ run = [
|
||||
"go clean -testcache",
|
||||
"mise cache clear",
|
||||
"rm -rf /tmp/railpack",
|
||||
# delete all top-level node_modules in examples/, this can cause state issues which cause snapshots to generate incorrectly
|
||||
"find examples -maxdepth 2 -type d -name node_modules -exec rm -rf {} +",
|
||||
# same goes for python .venv folders
|
||||
"find examples -maxdepth 2 -type d -name .venv -exec rm -rf {} +",
|
||||
]
|
||||
|
||||
# if a example folder does not have a test.json file, it will be skipped
|
||||
|
||||
Reference in New Issue
Block a user