├── .gitignore ├── cobol ├── examples │ ├── ebay.cobol │ ├── data_gov.cobol │ ├── vercel.cobol │ ├── google.cobol │ ├── indeed.cobol │ ├── buzzfeed.cobol │ ├── foxnews.cobol │ ├── nytimes.cobol │ ├── 33across.cobol │ ├── whitehouse.cobol │ ├── yahoo_finance.cobol │ ├── etherscan.cobol │ ├── hackernews.cobol │ ├── hn_algolia.cobol │ ├── youtube.cobol │ ├── wikipedia.cobol │ ├── instagram.cobol │ ├── sec.cobol │ ├── marginalia.cobol │ ├── apple_investors.cobol │ ├── apple.cobol │ ├── example.cobol │ ├── reddit.cobol │ ├── yellow_pages.cobol │ ├── bing.cobol │ ├── nist.cobol │ ├── internet_archive.cobol │ ├── arxiv.cobol │ └── espn.cobol ├── template.go ├── cobol.go └── README.md ├── Makefile ├── browsers ├── README.md ├── browsers.go └── constants.go ├── go.mod ├── vercel ├── create_and_deploy.go ├── projects.go ├── README.md ├── client.go └── deploy.go ├── go.sum ├── LICENSE ├── main.go ├── browserbased ├── example.go ├── browserbased.go └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | browserbased-bin -------------------------------------------------------------------------------- /cobol/examples/ebay.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://ebay.com -------------------------------------------------------------------------------- /cobol/examples/data_gov.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://data.gov/ -------------------------------------------------------------------------------- /cobol/examples/vercel.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://vercel.com -------------------------------------------------------------------------------- /cobol/examples/google.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.google.com -------------------------------------------------------------------------------- /cobol/examples/indeed.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.indeed.com/ -------------------------------------------------------------------------------- /cobol/examples/buzzfeed.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.buzzfeed.com/ -------------------------------------------------------------------------------- /cobol/examples/foxnews.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.foxnews.com/ -------------------------------------------------------------------------------- /cobol/examples/nytimes.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.nytimes.com/ -------------------------------------------------------------------------------- /cobol/examples/33across.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.33across.com/news/ -------------------------------------------------------------------------------- /cobol/examples/whitehouse.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.whitehouse.gov/ -------------------------------------------------------------------------------- /cobol/examples/yahoo_finance.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://finance.yahoo.com/ -------------------------------------------------------------------------------- /cobol/examples/etherscan.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://etherscan.io/block/20696459 -------------------------------------------------------------------------------- /cobol/examples/hackernews.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://news.ycombinator.com 2 | -------------------------------------------------------------------------------- /cobol/examples/hn_algolia.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://hn.algolia.com -------------------------------------------------------------------------------- /cobol/examples/youtube.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.youtube.com/@pirate-wires -------------------------------------------------------------------------------- /cobol/examples/wikipedia.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://en.wikipedia.org/wiki/Project_Xanadu -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run main.go 3 | build: 4 | go build -o browserbased-bin main.go 5 | -------------------------------------------------------------------------------- /cobol/examples/instagram.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://www.instagram.com/joerogan/?hl=en -------------------------------------------------------------------------------- /cobol/examples/sec.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent -------------------------------------------------------------------------------- /cobol/examples/marginalia.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | -- DISABLE JAVASCRIPT 3 | NAVIGATE TO https://search.marginalia.nu/ -------------------------------------------------------------------------------- /cobol/examples/apple_investors.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://investor.apple.com/investor-relations/default.aspx -------------------------------------------------------------------------------- /cobol/examples/apple.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://d18rn0p25nwr6d.cloudfront.net/CIK-0000320193/0a22998b-782a-4361-8021-6f80011563da.html 2 | -------------------------------------------------------------------------------- /cobol/examples/example.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://news.ycombinator.com 3 | CLICK ON span.pagetop:nth-child(1) a:nth-child(2) 4 | -------------------------------------------------------------------------------- /cobol/examples/reddit.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | -- Will give you a funny blocked view 3 | DISABLE JAVASCRIPT 4 | NAVIGATE TO https://old.reddit.com/r/memes/ -------------------------------------------------------------------------------- /cobol/examples/yellow_pages.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://www.yellowpages.com/costa-mesa-ca/mip/avilas-el-ranchito-costa-mesa-9635669?lid=1001769947772 -------------------------------------------------------------------------------- /cobol/examples/bing.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://bing.com 3 | ENTER INTO textarea#sb_form_q "memes" 4 | ENTER INTO textarea#sb_form_q "" -------------------------------------------------------------------------------- /cobol/examples/nist.cobol: -------------------------------------------------------------------------------- 1 | main: 2 | NAVIGATE TO https://www.nist.gov/publications/line-coupling-hollow-fiber-flow-field-flow-fractionation-and-depolarized-multi-angle -------------------------------------------------------------------------------- /cobol/examples/internet_archive.cobol: -------------------------------------------------------------------------------- 1 | NAVIGATE TO https://web.archive.org/ 2 | -- NAVIGATE TO https://web.archive.org/web/20240000000000*/https://www.cia.gov/the-world-factbook/ -------------------------------------------------------------------------------- /cobol/examples/arxiv.cobol: -------------------------------------------------------------------------------- 1 | -- NAVIGATE TO https://arxiv.org/list/math.CT/recent 2 | -- NAVIGATE TO https://arxiv.org/abs/2408.16775 3 | NAVIGATE TO https://arxiv.org/list/q-bio.GN/recent -------------------------------------------------------------------------------- /cobol/examples/espn.cobol: -------------------------------------------------------------------------------- 1 | -- NAVIGATE TO https://www.espn.com/nfl/scoreboard 2 | --NAVIGATE TO https://www.espn.com/college-football/game/_/gameId/401437033 3 | NAVIGATE TO https://www.espn.com/nba/stats -------------------------------------------------------------------------------- /browsers/README.md: -------------------------------------------------------------------------------- 1 | # Browsers 2 | 3 | This contains the project configuration necessary for a Next.JS server running a puppeteer function. Credit goes to [this guy](https://gist.github.com/kettanaito/56861aff96e6debc575d522dd03e5725?permalink_comment_id=5010934#gistcomment-5010934) for getting a solution working in 2024 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yevbar/browserbased 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 7 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 8 | github.com/urfave/cli/v2 v2.27.4 // indirect 9 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /vercel/create_and_deploy.go: -------------------------------------------------------------------------------- 1 | package vercel 2 | 3 | func CreateAndDeploy(vercelToken string, files map[string]string) (*VercelDeploymentResponse, error) { 4 | client := CreateClient(vercelToken) 5 | 6 | project, err := client.CreateNewProject(GenerateProjectName()) 7 | 8 | if err != nil { 9 | return nil, err 10 | } 11 | 12 | return client.CreateNewDeployment(project.Name, files) 13 | } 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= 6 | github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= 7 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 8 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 9 | -------------------------------------------------------------------------------- /browsers/browsers.go: -------------------------------------------------------------------------------- 1 | package browsers 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/yevbar/browserbased/vercel" 7 | ) 8 | 9 | // Credit to this guy for figuring out how to get puppeteer working on Vercel in 2024 10 | // https://gist.github.com/kettanaito/56861aff96e6debc575d522dd03e5725?permalink_comment_id=5010934#gistcomment-5010934 11 | func CreateFilesystemFromScript(puppeteerScript string) map[string]string { 12 | fs := map[string]string{ 13 | "src/app/api/route.ts": puppeteerScript, 14 | "next.config.mjs": NEXT_CONFIG_MJS, 15 | "package.json": PACKAGE_JSON, 16 | "package-lock.json": PACKAGE_LOCK_JSON, 17 | } 18 | 19 | return fs 20 | } 21 | 22 | func SpinUpPuppeteerEndpoint(puppeteerScript string) string { 23 | vercelToken := os.Getenv("VERCEL_TOKEN") 24 | if len(vercelToken) == 0 { 25 | panic("The [VERCEL_TOKEN] environment variable must be specified") 26 | } 27 | 28 | deployment, err := vercel.CreateAndDeploy(vercelToken, CreateFilesystemFromScript(puppeteerScript)) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | return deployment.URL 34 | } 35 | -------------------------------------------------------------------------------- /vercel/projects.go: -------------------------------------------------------------------------------- 1 | package vercel 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | const PROJECTS_URL = "https://api.vercel.com/v10/projects" 10 | const BUILD_COMMAND = "npm run build" 11 | const VERCEL_FRAMEWORK = "nextjs" 12 | 13 | func GenerateProjectName() string { 14 | currentTime := strconv.Itoa(int(time.Now().Unix())) 15 | return fmt.Sprintf("project_%s", currentTime) 16 | } 17 | 18 | type VercelCreateProjectResponse struct { 19 | AccountID string `json:"accountId"` 20 | DirectoryListing bool `json:"directoryListing"` 21 | ID string `json:"id"` 22 | Name string `json:"name"` 23 | NodeVersion string `json:"nodeVersion"` 24 | } 25 | 26 | func (v *VercelHTTPClient) CreateNewProject(projectName string) (*VercelCreateProjectResponse, error) { 27 | response := VercelCreateProjectResponse{} 28 | 29 | err := v.MakePostRequest(PROJECTS_URL, map[string]interface{}{}, map[string]interface{}{ 30 | "name": projectName, 31 | "buildCommand": BUILD_COMMAND, 32 | "framework": VERCEL_FRAMEWORK, 33 | }, &response) 34 | 35 | return &response, err 36 | } 37 | -------------------------------------------------------------------------------- /vercel/README.md: -------------------------------------------------------------------------------- 1 | # Vercel API 2 | 3 | There wasn't a Golang SDK or wrapper for Vercel's REST API so I put this together for the endpoints I was interested in 4 | 5 | ## Methods 6 | 7 | To programatically deploy a function to Vercel, you just need an [access token from Vercel](https://vercel.com/account/settings/tokens) 8 | 9 | ```golang 10 | // main.go 11 | 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | 17 | "github.com/yevbar/browserbased/vercel" 18 | ) 19 | 20 | func main() { 21 | deployment, err := vercel.CreateAndDeploy("", map[string]string{ // object representing filesystem mapping filepath to contents 22 | "package.json": "", 23 | "package-lock.json": "", 24 | "some/path/to/page.ts": "", 25 | }) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("App is deployed to: %s\n", deployment.URL) 31 | } 32 | ``` 33 | 34 | The way it works is by creating a new project then creating a deployment to go along with it. At the moment nothing is being done to re-use prior deployments cause I think it's funnier that way 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/urfave/cli/v2" 10 | "github.com/yevbar/browserbased/browserbased" 11 | ) 12 | 13 | func DeployFromFile(COBOLScriptPath string) *browserbased.BrowserbasedBrowser { 14 | cobol, err := os.ReadFile(COBOLScriptPath) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | fmt.Println("Deploying a browserbased browser!") 20 | browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{ 21 | COBOLScript: string(cobol), 22 | }) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | fmt.Printf("Deployed to: %s\nTo access the browser go to %s\n", browser.DeployedURL, browser.BrowserURL) 28 | 29 | return browser 30 | } 31 | 32 | func main() { 33 | app := &cli.App{ 34 | Name: "deploy", 35 | Usage: "Deploy a COBOL script to a headless browser", 36 | Action: func(cCtx *cli.Context) error { 37 | path := cCtx.Args().Get(0) 38 | if len(path) == 0 { 39 | fmt.Println("You must provide a path to a COBOL script to deploy it") 40 | return nil 41 | } 42 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { 43 | fmt.Printf("The path you provided [%s] is invalid, please provide a valid path to a COBOL script to deploy it\n", path) 44 | return nil 45 | } 46 | DeployFromFile(path) 47 | return nil 48 | }, 49 | } 50 | 51 | if err := app.Run(os.Args); err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /browserbased/example.go: -------------------------------------------------------------------------------- 1 | package browserbased 2 | 3 | // A barebones Next.js function that navigates to example.com and takes a screenshot 4 | const PUPPETEER_SCRIPT = ` 5 | import { NextRequest, NextResponse } from "next/server"; 6 | import puppeteerCore from "puppeteer-core"; 7 | import puppeteer from "puppeteer"; 8 | import chromium from "@sparticuz/chromium"; 9 | 10 | export const dynamic = "force-dynamic"; 11 | 12 | async function getBrowser() { 13 | if (process.env.VERCEL_ENV === "production") { 14 | const executablePath = await chromium.executablePath(); 15 | 16 | const browser = await puppeteerCore.launch({ 17 | args: chromium.args, 18 | defaultViewport: chromium.defaultViewport, 19 | executablePath, 20 | headless: chromium.headless, 21 | }); 22 | return browser; 23 | } else { 24 | const browser = await puppeteer.launch(); 25 | return browser; 26 | } 27 | } 28 | 29 | export async function GET(request: NextRequest) { 30 | const browser = await getBrowser(); 31 | const page = await browser.newPage(); 32 | await page.goto("https://example.com"); 33 | const pdf = await page.pdf(); 34 | await browser.close(); 35 | return new NextResponse(pdf, { 36 | headers: { 37 | "Content-Type": "application/pdf", 38 | }, 39 | }); 40 | } 41 | ` 42 | 43 | // Returns a barebones Next.js function that navigates to example.com and takes a screenshot 44 | func ExamplePuppeteerScript() string { 45 | return PUPPETEER_SCRIPT 46 | } 47 | -------------------------------------------------------------------------------- /vercel/client.go: -------------------------------------------------------------------------------- 1 | package vercel 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type VercelHTTPClient struct { 12 | BearerToken string 13 | } 14 | 15 | func CreateClient(bearerToken string) *VercelHTTPClient { 16 | return &VercelHTTPClient{ 17 | BearerToken: bearerToken, 18 | } 19 | } 20 | 21 | func (v *VercelHTTPClient) MakePostRequest(providedURL string, queryParams map[string]interface{}, bodyParams map[string]interface{}, responseTarget interface{}) error { 22 | marshalled, _ := json.Marshal(bodyParams) 23 | 24 | formattedURL := providedURL 25 | if len(queryParams) > 0 { 26 | formattedURL += "?" 27 | for k, v := range queryParams { 28 | formattedURL += k + url.QueryEscape(fmt.Sprintf("%v", v)) + "&" 29 | } 30 | } 31 | 32 | r, err := http.NewRequest("POST", formattedURL, bytes.NewBuffer(marshalled)) 33 | if err != nil { 34 | return err 35 | } 36 | r.Header.Add("Content-type", "application/json") 37 | r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", v.BearerToken)) 38 | 39 | client := &http.Client{} 40 | res, err := client.Do(r) 41 | if err != nil { 42 | return err 43 | } 44 | defer res.Body.Close() 45 | 46 | if responseTarget == nil { 47 | var x map[string]interface{} 48 | json.NewDecoder(res.Body).Decode(&x) 49 | fmt.Println("Got the following for x") 50 | fmt.Printf("%+v\n", x) 51 | return nil 52 | } 53 | 54 | return json.NewDecoder(res.Body).Decode(responseTarget) 55 | } 56 | -------------------------------------------------------------------------------- /cobol/template.go: -------------------------------------------------------------------------------- 1 | package cobol 2 | 3 | const PUPPETEER_TEMPLATE = ` 4 | import { NextRequest, NextResponse } from "next/server"; 5 | import puppeteerCore from "puppeteer-core"; 6 | import puppeteer from "puppeteer"; 7 | import chromium from "@sparticuz/chromium"; 8 | 9 | chromium.setHeadlessMode = true 10 | chromium.setGraphicsMode = false 11 | 12 | export const dynamic = "force-dynamic"; 13 | 14 | async function getBrowser() { 15 | if (process.env.VERCEL_ENV === "production") { 16 | const executablePath = await chromium.executablePath(); 17 | 18 | const browser = await puppeteerCore.launch({ 19 | args: chromium.args, 20 | defaultViewport: chromium.defaultViewport, 21 | executablePath, 22 | headless: chromium.headless, 23 | }); 24 | return browser; 25 | } else { 26 | const browser = await puppeteer.launch(); 27 | return browser; 28 | } 29 | } 30 | 31 | export async function GET(request: NextRequest) { 32 | const browser = await getBrowser(); 33 | const page = await browser.newPage(); 34 | await page.setRequestInterception(true); 35 | 36 | page.on('request', (req) => { 37 | if(req.resourceType() === 'image' || req.resourceType() === 'stylesheet' || req.resourceType() === 'font'){ 38 | req.abort(); 39 | } else { 40 | req.continue(); 41 | } 42 | }) 43 | %s 44 | const pdf = await page.pdf({format: 'A4', printBackground: true}); 45 | await browser.close(); 46 | return new NextResponse(pdf, { 47 | headers: { 48 | "Content-Type": "application/pdf", 49 | }, 50 | }); 51 | } 52 | ` 53 | -------------------------------------------------------------------------------- /browserbased/browserbased.go: -------------------------------------------------------------------------------- 1 | package browserbased 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/yevbar/browserbased/browsers" 8 | "github.com/yevbar/browserbased/cobol" 9 | ) 10 | 11 | // BrowserbasedBrowser contains information about the deployed service containing a headless browser function 12 | type BrowserbasedBrowser struct { 13 | DeployedURL string // The deployed URL of the serverless function 14 | BrowserURL string // The URL to access the browser 15 | } 16 | 17 | // BrowserbasedBrowserConfig contains information about the service to deploy containing a headless browser function 18 | type BrowserbasedBrowserConfig struct { 19 | COBOLScript string // If provided, takes precedent over `PuppeteerScript` 20 | PuppeteerScript string // If not provided, an example script navigating to example.com and taking a screenshot will be used 21 | } 22 | 23 | // CreateBrowserbasedBrowser takes a configuration and spins up a service containing a headless browser function 24 | func CreateBrowserbasedBrowser(config *BrowserbasedBrowserConfig) (*BrowserbasedBrowser, error) { 25 | puppeterSource := config.PuppeteerScript 26 | if len(config.COBOLScript) > 0 { 27 | puppeterSource = cobol.COBOLToPuppeteer(config.COBOLScript) 28 | } 29 | if len(puppeterSource) == 0 { 30 | puppeterSource = ExamplePuppeteerScript() 31 | } 32 | 33 | deployedURL := browsers.SpinUpPuppeteerEndpoint(puppeterSource) 34 | if !strings.HasPrefix(deployedURL, "https://") { 35 | deployedURL = fmt.Sprintf("https://%s", deployedURL) 36 | } 37 | 38 | return &BrowserbasedBrowser{ 39 | DeployedURL: deployedURL, 40 | BrowserURL: fmt.Sprintf("%s/api", deployedURL), 41 | }, nil 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browserbased 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/yevbar/browserbased.svg)](https://pkg.go.dev/github.com/yevbar/browserbased) 4 | 5 | * [Overview](#overview) 6 | * [Configuring](#configuring) 7 | * [Contents](#contents) 8 | * [Limitations](#limitations) 9 | * [Inspiration](#inspiration) 10 | 11 | ## Overview 12 | 13 | Open-source serverless headless browsers 14 | 15 | At the time of writing this, here's an example output from the [wikipedia](https://github.com/yevbar/browserbased/blob/master/cobol/examples/wikipedia.cobol) example [https://project1725750744.vercel.app/api](https://project1725750744.vercel.app/api) 16 | 17 | ## Configuring 18 | 19 | Prior to running, you'll need to have a `VERCEL_TOKEN` environment variable set up with an access token which you can obtain [here](https://vercel.com/account/settings/tokens) 20 | 21 | ```bash 22 | $ export VERCEL_TOKEN='abc123' 23 | ``` 24 | 25 | ## Contents 26 | 27 | * If you're interested in spinning up serverless headless browsers, check out the [browserbased module](https://github.com/yevbar/browserbased/blob/master/browserbased/) 28 | * If you're interested in how it works, check out the [browser module](https://github.com/yevbar/browserbased/blob/master/browsers/) 29 | * If you're interested in an instruction language for browsers, check out [COBOL](https://github.com/yevbar/browserbased/blob/master/cobol/) 30 | * If you're interested in programatically deploying to Vercel using Golang, check out the [vercel module](https://github.com/yevbar/browserbased/blob/master/vercel/) 31 | 32 | ## Limitations 33 | 34 | This does not offer any stealth or anti-anti-scraping capabilities and is as good as you can make your COBOL/Puppeteer scripts 35 | 36 | ## Inspiration 37 | 38 | ["Try running one on a lambda, I dare you"](https://www.youtube.com/watch?v=us_vS2EVDOA&t=46s) 39 | -------------------------------------------------------------------------------- /vercel/deploy.go: -------------------------------------------------------------------------------- 1 | package vercel 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | const DEPLOYMENT_URL = "https://api.vercel.com/v13/deployments" 10 | 11 | func GenerateDeploymentName() string { 12 | currentTime := strconv.Itoa(int(time.Now().Unix())) 13 | return fmt.Sprintf("deployment_%s", currentTime) 14 | } 15 | 16 | type VercelDeploymentFile struct { 17 | Data string `json:"data"` 18 | Encoding string `json:"encoding"` // Either 'base64' or 'utf-8' 19 | File string `json:"file"` // The filename including the whole path ie folder/file.js 20 | } 21 | 22 | func FilesToVercelDeploymentFiles(files map[string]string) []*VercelDeploymentFile { 23 | result := []*VercelDeploymentFile{} 24 | 25 | for filePath, fileContents := range files { 26 | result = append(result, &VercelDeploymentFile{ 27 | Data: fileContents, 28 | Encoding: "utf-8", 29 | File: filePath, 30 | }) 31 | } 32 | 33 | return result 34 | } 35 | 36 | type VercelDeploySettings struct { 37 | BuildCommand *string `json:"buildCommand,omitempty"` // Could be null in response hence pointer 38 | InstallCommand *string `json:"installCommand"` // Ditto ^ 39 | Framework *string `json:"framework,omitempty"` // Ditto ^ 40 | } 41 | 42 | func DefaultVercelDeploySettings() VercelDeploySettings { 43 | buildCommand := BUILD_COMMAND 44 | return VercelDeploySettings{ 45 | BuildCommand: &buildCommand, 46 | } 47 | } 48 | 49 | type VercelBuild struct { 50 | Env []interface{} `json:"env"` 51 | } 52 | 53 | type VercelCreator struct { 54 | UID string `json:"uid"` 55 | } 56 | 57 | type VercelDeploymentResponse struct { 58 | AliasAssigned bool `json:"aliasAssigned"` 59 | BootedAt int `json:"bootedAt"` 60 | Build VercelBuild `json:"build"` 61 | BuildSkipped bool `json:"buildSkipped"` 62 | BuildingAt int `json:"buildingAt"` 63 | CreatedAt int `json:"createdAt"` 64 | CreatedIn string `json:"createdIn"` 65 | Creator VercelCreator `json:"creator"` 66 | Env []interface{} `json:"env"` 67 | ID string `json:"id"` 68 | InspectorURL *string `json:"inspectorUrl"` // Is string | null hence the pointer 69 | IsInCurrentBuildsQueue bool `json:"isInConcurrentBuildsQueue"` 70 | Meta map[string]interface{} `json:"meta"` 71 | Name string `json:"name"` 72 | OwnerID string `json:"ownerId"` 73 | Plan string `json:"plan"` // One of "pro", "enterprise", "hobby" 74 | ProjectID string `json:"projectId"` 75 | ProjectSettings VercelDeploySettings `json:"projectSettings"` 76 | Public bool `json:"public"` 77 | ReadyState string `json:"readyState"` // One of "CANCELED", "ERROR", "QUEUED", "BUILDING", "INITIALIZING", "READY" 78 | Regions []interface{} `json:"regions"` 79 | Routes *[]interface{} `json:"routes"` 80 | Status string `json:"status"` // One of "CANCELED", "ERROR", "QUEUED", "BUILDING", "INITIALIZING", "READY" 81 | Type string `json:"type"` // Should just be "LAMBDAS" 82 | URL string `json:"url"` 83 | Version int `json:"version"` 84 | } 85 | 86 | // Creates a deployment based on a dictionary mapping filepath to filecontent 87 | func (v *VercelHTTPClient) CreateNewDeployment(projectName string, files map[string]string) (*VercelDeploymentResponse, error) { 88 | response := VercelDeploymentResponse{} 89 | 90 | err := v.MakePostRequest(DEPLOYMENT_URL, map[string]interface{}{ 91 | "forceNew": 1, 92 | "skipAutoDetectionConfirmation": 1, 93 | }, map[string]interface{}{ 94 | "name": GenerateDeploymentName(), 95 | "files": FilesToVercelDeploymentFiles(files), 96 | "project": projectName, 97 | "projectSettings": DefaultVercelDeploySettings(), 98 | "target": "production", 99 | }, &response) 100 | 101 | return &response, err 102 | } 103 | -------------------------------------------------------------------------------- /cobol/cobol.go: -------------------------------------------------------------------------------- 1 | package cobol 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func StringToCharByCharPuppeteer(s string) string { 9 | result := "" 10 | 11 | if s == "" { 12 | return "await page.keyboard.press('Enter');" 13 | } 14 | 15 | for i := 0; i < len(s); i++ { 16 | curChar := s[i:i+1] 17 | result += "await page.keyboard.sendCharacter('" + curChar + "');\n" 18 | } 19 | 20 | return result 21 | } 22 | 23 | func COBOLLineToPuppeteer(line string) string { 24 | trimmed := strings.TrimSpace(line) 25 | split := []string{} 26 | 27 | curToken := "" 28 | for i := 0; i < len(trimmed); i++ { 29 | curChar := trimmed[i:i+1] 30 | if len(strings.TrimSpace(curChar)) == 0 { 31 | if len(curToken) > 0 { 32 | split = append(split, curToken) 33 | curToken = "" 34 | } 35 | continue 36 | } 37 | curToken += curChar 38 | } 39 | if len(strings.TrimSpace(curToken)) > 0 { 40 | split = append(split, curToken) 41 | } 42 | 43 | if len(split) == 0 { 44 | return "" 45 | } 46 | 47 | withoutComment := []string{} 48 | for _, token := range split { 49 | if strings.HasPrefix(token, "--") { 50 | break 51 | } 52 | withoutComment = append(withoutComment, strings.ReplaceAll(token, "\"", "'")) 53 | } 54 | split = withoutComment 55 | 56 | if len(split) == 0 { 57 | return "" 58 | } 59 | 60 | switch command := split[0]; command { 61 | case "NAVIGATE": 62 | if len(split) == 1 { 63 | return "" 64 | } 65 | if split[1] != "TO" { 66 | return "" 67 | } 68 | if split[2] == "NOTHING" { 69 | return "" 70 | } 71 | return fmt.Sprintf("await page.goto(\"%s\");", split[2]) 72 | case "CLICK": 73 | if len(split) == 1 { 74 | return "" 75 | } 76 | if split[1] != "ON" { 77 | return "" 78 | } 79 | if split[2] == "NOTHING" { 80 | return "" 81 | } 82 | return fmt.Sprintf("await page.click(\"%s\");", strings.Join(split[2:], " ")) 83 | case "ENTER": 84 | if len(split) == 1 { 85 | return "" 86 | } 87 | if split[1] != "INTO" { 88 | return "" 89 | } 90 | remString := strings.Join(split[2:], " ") 91 | quoteIndex := strings.Index(remString, "'") 92 | if quoteIndex < 0 { 93 | return "" 94 | } 95 | selector := strings.TrimSpace(remString[:quoteIndex]) 96 | text := remString[quoteIndex+1:len(remString)-1] 97 | if text == "" { 98 | return StringToCharByCharPuppeteer(text) 99 | } 100 | hashtagIndex := strings.Index(selector, "#") 101 | 102 | if strings.HasPrefix(selector, "textarea") && hashtagIndex > 0 { 103 | return fmt.Sprintf( 104 | strings.Join([]string{ 105 | fmt.Sprintf("await page.click(\"%s\");", selector), 106 | StringToCharByCharPuppeteer(text), 107 | }, "\n"), 108 | ) 109 | } 110 | return fmt.Sprintf("await page.type(\"%s\", \"%s\")", selector, text) 111 | case "DISABLE": 112 | if len(split) == 1 { 113 | return "" 114 | } 115 | if split[1] != "JAVASCRIPT" { 116 | return "" 117 | } 118 | return `await page.setRequestInterception(true); 119 | page.on('request', request => (request.resourceType() === 'script') ? request.abort() : request.continue());` 120 | case "GO": 121 | if len(split) == 1 { 122 | return "" 123 | } 124 | if split[1] != "BACK" { 125 | return "" 126 | } 127 | return "await page.goBack();\n" 128 | case "GOTO": 129 | if len(split) == 1 { 130 | return "" 131 | } 132 | return fmt.Sprintf("await %s();", split[1]) 133 | case "HOVER": 134 | if len(split) == 1 { 135 | return "" 136 | } 137 | if split[1] != "OVER" { 138 | return "" 139 | } 140 | return fmt.Sprintf("await page.hover(\"%s\");", strings.Join(split[2:], " ")) 141 | default: 142 | return "" 143 | } 144 | } 145 | 146 | func COBOLBlockToPuppeteer(lines []string) string { 147 | result := "" 148 | 149 | for _, line := range lines { 150 | result += fmt.Sprintf("%s\n", COBOLLineToPuppeteer(line)) 151 | } 152 | 153 | trimmed := strings.TrimSpace(result) 154 | return trimmed 155 | } 156 | 157 | // Given a COBOL program, generates a block controlling a browser 158 | func COBOLToPuppeteer(cobolScript string) string { 159 | result := "" 160 | 161 | blocks := COBOLToBlocks(cobolScript) 162 | for blockID, blockDef := range blocks { 163 | if blockID == "main" { 164 | continue 165 | } 166 | 167 | // Define function with block id as symbol and then COBOLToPuppeteer(blockDef) as body definition 168 | result += fmt.Sprintf("async function %s() {\n%s\n}\n", blockID, COBOLBlockToPuppeteer(blockDef)) 169 | } 170 | 171 | if mainDef, ok := blocks["main"]; ok { 172 | result += COBOLBlockToPuppeteer(mainDef) 173 | } 174 | 175 | return fmt.Sprintf(PUPPETEER_TEMPLATE, result) 176 | } 177 | 178 | // Given a COBOL program, creates a map from block identifier to block definition 179 | func COBOLToBlocks(cobolScript string) map[string][]string { 180 | result := map[string][]string{} 181 | currentBlock := "main" 182 | 183 | lines := strings.Split(cobolScript, "\n") 184 | for _, line := range lines { 185 | trimmed := strings.TrimSpace(line) 186 | if len(trimmed) == 0 { 187 | continue 188 | } else if line[len(line) - 1:] == ":" { 189 | // Start new block 190 | currentBlock = line[:strings.Index(line, ":")] 191 | } else if len(trimmed) == 0 { 192 | // Just blank space 193 | continue 194 | } else { 195 | // Something that belongs in the current block definition 196 | if currentBlockDef, ok := result[currentBlock]; ok { 197 | result[currentBlock] = append(currentBlockDef, trimmed) 198 | } else { 199 | result[currentBlock] = []string{trimmed} 200 | } 201 | } 202 | } 203 | 204 | return result 205 | } 206 | -------------------------------------------------------------------------------- /cobol/README.md: -------------------------------------------------------------------------------- 1 | # COmmon Browser Oriented Language 2 | 3 | * [What is this?](#what-is-this) 4 | * [How does it work?](#how-does-it-work) 5 | * [Known working examples (free tier)](#known-working-examples-vercel-free-tier) 6 | * [Syntax](#syntax) 7 | * [Failure tolerance](#failure-tolerance) 8 | * [Keywords](#keywords) 9 | * [NAVIGATE](#navigate) 10 | * [CLICK](#click) 11 | * [ENTER](#enter) 12 | * [BACK](#back) 13 | * [HOVER](#hover) 14 | * [NOTHING](#nothing) 15 | * [Functions](#functions) 16 | * [Comments](#comments) 17 | 18 | ## What is this? 19 | 20 | If you've ever thought writing programs involving headless browsers were tedious or verbose and wished there were a batteries-included framework that'd strip away some of the manual work, you've come to the right place. 21 | 22 | If you're interested in using COBOL rather than getting the conceptual digest, check out the [browserbased module](https://github.com/yevbar/browserbased/blob/master/browserbased/README.md#control-browsers-using-cobol) 23 | 24 | ## How does it work? 25 | 26 | [Click here if you'd rather look at code examples](#known-working-examples-free-tier) 27 | 28 | To understand the failure tolerance of COBOL, it may be helpful to look at the following topics from this lens: 29 | 30 | * [Garbage collection](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)) is an abstraction over memory management 31 | * [Haxl](https://www.youtube.com/watch?v=sT6VJkkhy0o) is an abstraction over concurrency 32 | * **NOTHING** is an abstraction over stupidity 33 | 34 | COBOL can be understood as an instruction language like something you'd see in a computer architecture class but for controlling a web browser. Here's an example script for going to Google, entering a query into the search bar, then clicking on the button to invoke a search request: 35 | 36 | ``` 37 | NAVIGATE TO https://google.com 38 | ENTER INTO input#search-box "your query" 39 | CLICK ON button.cta 40 | ``` 41 | 42 | ## Known working examples (Vercel free tier) 43 | 44 | These are some of the ones I was able to get working (if it doesn't work on the first request, try invoking the `/api` endpoint once to warm up the function then requesting a 2nd time) 45 | 46 | - [NIST abstracts](https://github.com/yevbar/browserbased/blob/master/cobol/examples/nist.cobol) 47 | - [arxiv](https://github.com/yevbar/browserbased/blob/master/cobol/examples/arxiv.cobol) 48 | - [Wikipedia](https://github.com/yevbar/browserbased/blob/master/cobol/examples/wikipedia.cobol) 49 | - [Hacker News](https://github.com/yevbar/browserbased/blob/master/cobol/examples/hackernews.cobol) 50 | 51 | You can see the examples folder of scripts I was working on and some of them may actually work on a paid Vercel plan. Leaving for others to toy with 52 | 53 | ## Syntax 54 | 55 | COBOL is newline-sensitive and doesn't care how you indent commands so long as each line accomplishes a single instruction. The goal of COBOL is to succinctly convey browser actions not code golf browsing. 56 | 57 | ### Failure tolerance 58 | 59 | Keyword commands must have all words be correct 60 | 61 | ``` 62 | NAVIGATE TO https://example.com -- Actually works 63 | NAVIGATE TOWARD https://example.com -- Nah 64 | 65 | ENTER IN input#search-box "your query" -- 2nd word after ENTER must be INTO so this line does nothing 66 | ENTER INTO input#search-box "your query" -- Actually works as intended 67 | ``` 68 | 69 | Malformed COBOL lines are treated similar to [NOTHING](#nothing) and are simply ignored 70 | 71 | ### Keywords 72 | 73 | At the moment you can specify a browser to do stuff like the following 74 | 75 | ``` 76 | NAVIGATE TO 77 | CLICK ON 78 | ENTER INTO "" 79 | ``` 80 | 81 | Here are available keywords in COBOL 82 | 83 | #### NAVIGATE 84 | 85 | The `NAVIGATE` instruction tells a browser to navigate someplace, the syntax for this command is as follows 86 | 87 | ``` 88 | NAVIGATE TO 89 | ``` 90 | 91 | The `` gets provided to Puppeteer's [goto](https://pptr.dev/api/puppeteer.page.goto) method 92 | 93 | #### CLICK 94 | 95 | The `CLICK` instruction tells a browser to click on some element, the syntax is as follows 96 | 97 | ``` 98 | CLICK ON 99 | ``` 100 | 101 | The `` gets provided to Puppeteer's [click](https://pptr.dev/api/puppeteer.page.click) method 102 | 103 | #### ENTER 104 | 105 | The `ENTER` instruction tells a browser to enter some text into some element, the syntax is as follows 106 | 107 | ``` 108 | ENTER INTO "" 109 | ``` 110 | 111 | The `` and `` get provided to either Puppeteer's [type](https://pptr.dev/api/puppeteer.page.type) or [sendCharacter](https://pptr.dev/api/puppeteer.keyboard.sendcharacter) depending on whether the target element to type into is an input or textarea 112 | 113 | #### BACK 114 | 115 | The `BACK` instruction tells a browser to go back a page, the syntax is as follows 116 | 117 | ``` 118 | GO BACK 119 | ``` 120 | 121 | If just the expression `BACK` is provided or some other prior word than `GO`, it'll ignore the statement because of COBOL's [failure tolerance](#failure-tolerance). Under the hood it's Puppeteer's [goBack](https://pptr.dev/api/puppeteer.page.goback) method 122 | 123 | #### HOVER 124 | 125 | The `HOVER` instruction tells a browser to hover over some element, the syntax is as follows 126 | 127 | ``` 128 | HOVER OVER 129 | ``` 130 | 131 | The `` gets provided to Puppeteer's [hover](https://pptr.dev/api/puppeteer.page.hover) method 132 | 133 | #### NOTHING 134 | 135 | As a mid-sentence exit valve, the language also features a `NOTHING` keyword in case you were to generate a line that doesn't make sense 136 | 137 | ``` 138 | NAVIGATE TO https://google.com 139 | CLICK ON NOTHING -- Was written too soon 140 | ENTER INTO input#search-box "your query" 141 | CLICK ON button.cta 142 | ``` 143 | 144 | When translating to Puppeteer, lines with `NOTHING` as the "target" are ignored like comments but with the intent of allowing possibly incorrect code to be provided 145 | 146 | ### Functions 147 | 148 | Functions are identified with colons and the function body is comprised of the lines that follow it. Function invocations are handled with `GOTO` statements with the `main` function being the entry point of the program 149 | 150 | ``` 151 | do_stuff: 152 | NAVIGATE TO https://news.ycombinator.com 153 | CLICK ON span.pagetop:nth-child(1) a:nth-child(2) -- Clicks on "New" 154 | GO BACK -- Navigates back 155 | 156 | main: 157 | GOTO do_stuff 158 | ``` 159 | 160 | Like Ruby/Python, you should assume a bleedover of context where scopes are effectively "flattened" when jumping from one function block to another 161 | 162 | ### Comments 163 | 164 | Comments are done via double dash following a whitespace 165 | 166 | ``` 167 | NAVIGATE TO https://google.com--not a comment 168 | NAVIGATE TO https://google.com -- is a comment 169 | NAVIGATE TO https://google.com --also a comment 170 | ``` 171 | -------------------------------------------------------------------------------- /browserbased/README.md: -------------------------------------------------------------------------------- 1 | # Browserbased 2 | 3 | Like [serverless](https://www.serverless.com/) but for headless browsers. If you've wanted to inexpensively run numerous headless browsers, here's how you can do that 4 | 5 | * [Deploy serverless browsers using CLI](#deploy-serverless-browsers-using-cli) 6 | * [Building](#building) 7 | * [Running](#running) 8 | * [Control browsers using COBOL](#control-browsers-using-cobol) 9 | * [Control browsers using a COBOL script](#control-browsers-using-cobol-script) 10 | * [Taking a screenshot](#taking-a-screenshot) 11 | * [Customizing the browser logic](#customizing-the-browser-logic) 12 | 13 | ## Deploy serverless browsers using CLI 14 | 15 | ### Building 16 | 17 | At the moment, you'll need to clone this repository and run the build script 18 | 19 | ```bash 20 | $ git clone https://github.com/yevbar/browserbased 21 | $ cd browserbased 22 | $ make build # Now you have ./browserbased-bin 23 | ``` 24 | 25 | If you do not want to install make on your machine, this is the command it's actually running to produce the `browserbased-bin` file 26 | 27 | ```bash 28 | $ go build -o browserbased-bin main.go 29 | ``` 30 | 31 | ### Running 32 | 33 | Suppose you wanted to make a browserbased browser [go to Wikipedia](https://github.com/yevbar/browserbased/blob/master/cobol/examples/wikipedia.cobol), you can do that with the following [COBOL](https://github.com/yevbar/browserbased/blob/master/cobol/README.md) 34 | 35 | ``` 36 | -- cobol/examples/wikipedia.cobol 37 | 38 | NAVIGATE TO https://en.wikipedia.org/wiki/Project_Xanadu 39 | ``` 40 | 41 | Here's what it looks like to run the executable locally 42 | 43 | ```bash 44 | $ ./browserbased-bin cobol/examples/wikipedia.cobol 45 | Deploying a browserbased browser! 46 | Deployed to: https://.vercel.app 47 | To access the browser go to https://.vercel.app/api 48 | ``` 49 | 50 | And, if you'd like to see a full script building, adding to `PATH`, and then running on a provided file 51 | 52 | ```bash 53 | $ git clone https://github.com/yevbar/browserbased 54 | $ cd browserbased 55 | $ make build # Or the go build command 56 | $ sudo mv browserbased-bin /usr/local/bin/browserbased # Or some other folder listed when you run [echo "$PATH"] in your terminal 57 | $ browserbased cobol/examples/wikipedia.cobol 58 | Deploying a browserbased browser! 59 | Deployed to: https://.vercel.app 60 | To access the browser go to https://.vercel.app/api 61 | ``` 62 | 63 | ## Control browsers using COBOL 64 | 65 | If you'd like to run a [COBOL](https://github.com/yevbar/browserbased/blob/master/cobol/README.md) script instead of manipulating an existing Puppeteer one without using the [CLI](#building) here's how you can do that 66 | 67 | First, install the dependency 68 | 69 | ```bash 70 | $ go get github.com/yevbar/browserbased/browserbased 71 | ``` 72 | 73 | Then you can write a file like so 74 | 75 | ```golang 76 | // main.go 77 | package main 78 | 79 | import ( 80 | "fmt" 81 | "github.com/yevbar/browserbased/browserbased" 82 | ) 83 | 84 | func main() { 85 | browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{ 86 | COBOLScript: "NAVIGATE TO https://news.ycombinator.com", 87 | }) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | fmt.Printf("Deployed to: %s\nTo access the browser go to %s\n", browser.DeployedURL, browser.BrowserURL) 93 | } 94 | ``` 95 | 96 | And run like so 97 | 98 | ```bash 99 | $ go run main.go 100 | ``` 101 | 102 | ## Control browsers using a COBOL script 103 | 104 | First, install the dependency 105 | 106 | ```bash 107 | $ go get github.com/yevbar/browserbased/browserbased 108 | ``` 109 | 110 | Reading from a COBOL file can be done easily with `os` 111 | 112 | ```golang 113 | // main.go 114 | package main 115 | 116 | import ( 117 | "fmt" 118 | "os" 119 | 120 | "github.com/yevbar/browserbased/browserbased" 121 | ) 122 | 123 | func main() { 124 | cobolFilepath := "path/to/file.cobol" 125 | cobol, err := os.ReadFile(cobolFilepath) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{ 131 | COBOLScript: string(cobol), 132 | }) 133 | if err != nil { 134 | panic(err) 135 | } 136 | 137 | fmt.Printf("Deployed to: %s\nTo access the browser go to %s\n", browser.DeployedURL, browser.BrowserURL) 138 | } 139 | ``` 140 | 141 | ## Taking a screenshot 142 | 143 | The [example script](https://github.com/yevbar/browserbased/blob/master/browserbased/example.go) in the source code navigates to `https://example.com` and takes a screenshot of the page, here's the source code of the browser related stuff 144 | 145 | ```javascript 146 | const browser = await getBrowser(); 147 | const page = await browser.newPage(); 148 | await page.goto("https://example.com"); 149 | const pdf = await page.pdf(); 150 | await browser.close(); 151 | // Then returns the PDF as a response 152 | ``` 153 | 154 | To use it out of the box is pretty simple, here's what the Go source code for that could look like 155 | 156 | ```golang 157 | // main.go 158 | 159 | package main 160 | 161 | import ( 162 | "fmt" 163 | 164 | "github.com/yevbar/browserbased/browserbased" 165 | ) 166 | 167 | func main() { 168 | fmt.Println("Deploying a browserbased browser!") 169 | browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{}) 170 | if err != nil { 171 | panic(err) 172 | } 173 | 174 | fmt.Printf("Deployed to: %s\nTo access the browser go to %s\n", browser.DeployedURL, browser.BrowserURL) 175 | } 176 | ``` 177 | 178 | ## Customizing the browser logic 179 | 180 | To provide a script of your own to deploy, simply provide a `PuppeteerScript` string to the config object provided to `CreateBrowserbasedBrowser` 181 | 182 | ```diff 183 | // main.go 184 | 185 | package main 186 | 187 | import ( 188 | "fmt" 189 | 190 | "github.com/yevbar/browserbased/browserbased" 191 | ) 192 | 193 | func main() { 194 | fmt.Println("Deploying a browserbased browser!") 195 | - browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{}) 196 | + browser, err := browserbased.CreateBrowserbasedBrowser(&browserbased.BrowserbasedBrowserConfig{ 197 | + PuppeteerScript: "...", // Example shown below 198 | + }) 199 | if err != nil { 200 | panic(err) 201 | } 202 | 203 | fmt.Printf("Deployed to: %s\nTo access the browser go to %s\n", browser.DeployedURL, browser.BrowserURL) 204 | } 205 | ``` 206 | 207 | The `PupppeteerScript` value should look like the following with your changes being applied to the lines highlighted below in the `GET` function 208 | 209 | ```diff 210 | import { NextRequest, NextResponse } from "next/server"; 211 | import puppeteerCore from "puppeteer-core"; 212 | import puppeteer from "puppeteer"; 213 | import chromium from "@sparticuz/chromium"; 214 | 215 | export const dynamic = "force-dynamic"; 216 | 217 | async function getBrowser() { 218 | if (process.env.VERCEL_ENV === "production") { 219 | const executablePath = await chromium.executablePath(); 220 | 221 | const browser = await puppeteerCore.launch({ 222 | args: chromium.args, 223 | defaultViewport: chromium.defaultViewport, 224 | executablePath, 225 | headless: chromium.headless, 226 | }); 227 | return browser; 228 | } else { 229 | const browser = await puppeteer.launch(); 230 | return browser; 231 | } 232 | } 233 | 234 | export async function GET(request: NextRequest) { 235 | const browser = await getBrowser(); 236 | - const page = await browser.newPage(); 237 | - await page.goto("https://example.com"); 238 | - const pdf = await page.pdf(); 239 | - await browser.close(); 240 | - return new NextResponse(pdf, { 241 | - headers: { 242 | - "Content-Type": "application/pdf", 243 | - }, 244 | - }); 245 | + // Your changes here 246 | } 247 | ``` 248 | -------------------------------------------------------------------------------- /browsers/constants.go: -------------------------------------------------------------------------------- 1 | package browsers 2 | 3 | const NEXT_CONFIG_MJS = `/** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | experimental: { 6 | serverComponentsExternalPackages: ['puppeteer-core', '@sparticuz/chromium'], 7 | } 8 | }; 9 | 10 | export default nextConfig;` 11 | 12 | const PACKAGE_LOCK_JSON = `{ 13 | "name": "browser-test", 14 | "version": "1.0.0", 15 | "lockfileVersion": 3, 16 | "requires": true, 17 | "packages": { 18 | "": { 19 | "name": "browser-test", 20 | "version": "1.0.0", 21 | "license": "ISC", 22 | "dependencies": { 23 | "chrome-aws-lambda": "^10.1.0", 24 | "puppeteer-core": "^10.1.0" 25 | } 26 | }, 27 | "node_modules/@types/node": { 28 | "version": "22.5.4", 29 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", 30 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 31 | "optional": true, 32 | "dependencies": { 33 | "undici-types": "~6.19.2" 34 | } 35 | }, 36 | "node_modules/@types/yauzl": { 37 | "version": "2.10.3", 38 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", 39 | "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", 40 | "optional": true, 41 | "dependencies": { 42 | "@types/node": "*" 43 | } 44 | }, 45 | "node_modules/agent-base": { 46 | "version": "6.0.2", 47 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 48 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 49 | "dependencies": { 50 | "debug": "4" 51 | }, 52 | "engines": { 53 | "node": ">= 6.0.0" 54 | } 55 | }, 56 | "node_modules/balanced-match": { 57 | "version": "1.0.2", 58 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 59 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 60 | }, 61 | "node_modules/base64-js": { 62 | "version": "1.5.1", 63 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 64 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 65 | "funding": [ 66 | { 67 | "type": "github", 68 | "url": "https://github.com/sponsors/feross" 69 | }, 70 | { 71 | "type": "patreon", 72 | "url": "https://www.patreon.com/feross" 73 | }, 74 | { 75 | "type": "consulting", 76 | "url": "https://feross.org/support" 77 | } 78 | ] 79 | }, 80 | "node_modules/bl": { 81 | "version": "4.1.0", 82 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 83 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 84 | "dependencies": { 85 | "buffer": "^5.5.0", 86 | "inherits": "^2.0.4", 87 | "readable-stream": "^3.4.0" 88 | } 89 | }, 90 | "node_modules/brace-expansion": { 91 | "version": "1.1.11", 92 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 93 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 94 | "dependencies": { 95 | "balanced-match": "^1.0.0", 96 | "concat-map": "0.0.1" 97 | } 98 | }, 99 | "node_modules/buffer": { 100 | "version": "5.7.1", 101 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 102 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 103 | "funding": [ 104 | { 105 | "type": "github", 106 | "url": "https://github.com/sponsors/feross" 107 | }, 108 | { 109 | "type": "patreon", 110 | "url": "https://www.patreon.com/feross" 111 | }, 112 | { 113 | "type": "consulting", 114 | "url": "https://feross.org/support" 115 | } 116 | ], 117 | "dependencies": { 118 | "base64-js": "^1.3.1", 119 | "ieee754": "^1.1.13" 120 | } 121 | }, 122 | "node_modules/buffer-crc32": { 123 | "version": "0.2.13", 124 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 125 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 126 | "engines": { 127 | "node": "*" 128 | } 129 | }, 130 | "node_modules/chownr": { 131 | "version": "1.1.4", 132 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 133 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 134 | }, 135 | "node_modules/chrome-aws-lambda": { 136 | "version": "10.1.0", 137 | "resolved": "https://registry.npmjs.org/chrome-aws-lambda/-/chrome-aws-lambda-10.1.0.tgz", 138 | "integrity": "sha512-NZQVf+J4kqG4sVhRm3WNmOfzY0OtTSm+S8rg77pwePa9RCYHzhnzRs8YvNI6L9tALIW6RpmefWiPURt3vURXcw==", 139 | "dependencies": { 140 | "lambdafs": "^2.0.3" 141 | }, 142 | "engines": { 143 | "node": ">= 10.16" 144 | }, 145 | "peerDependencies": { 146 | "puppeteer-core": "^10.1.0" 147 | } 148 | }, 149 | "node_modules/concat-map": { 150 | "version": "0.0.1", 151 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 152 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 153 | }, 154 | "node_modules/debug": { 155 | "version": "4.3.1", 156 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 157 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 158 | "dependencies": { 159 | "ms": "2.1.2" 160 | }, 161 | "engines": { 162 | "node": ">=6.0" 163 | }, 164 | "peerDependenciesMeta": { 165 | "supports-color": { 166 | "optional": true 167 | } 168 | } 169 | }, 170 | "node_modules/devtools-protocol": { 171 | "version": "0.0.883894", 172 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.883894.tgz", 173 | "integrity": "sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg==" 174 | }, 175 | "node_modules/end-of-stream": { 176 | "version": "1.4.4", 177 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 178 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 179 | "dependencies": { 180 | "once": "^1.4.0" 181 | } 182 | }, 183 | "node_modules/extract-zip": { 184 | "version": "2.0.1", 185 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 186 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 187 | "dependencies": { 188 | "debug": "^4.1.1", 189 | "get-stream": "^5.1.0", 190 | "yauzl": "^2.10.0" 191 | }, 192 | "bin": { 193 | "extract-zip": "cli.js" 194 | }, 195 | "engines": { 196 | "node": ">= 10.17.0" 197 | }, 198 | "optionalDependencies": { 199 | "@types/yauzl": "^2.9.1" 200 | } 201 | }, 202 | "node_modules/fd-slicer": { 203 | "version": "1.1.0", 204 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 205 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 206 | "dependencies": { 207 | "pend": "~1.2.0" 208 | } 209 | }, 210 | "node_modules/find-up": { 211 | "version": "4.1.0", 212 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 213 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 214 | "dependencies": { 215 | "locate-path": "^5.0.0", 216 | "path-exists": "^4.0.0" 217 | }, 218 | "engines": { 219 | "node": ">=8" 220 | } 221 | }, 222 | "node_modules/fs-constants": { 223 | "version": "1.0.0", 224 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 225 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 226 | }, 227 | "node_modules/fs.realpath": { 228 | "version": "1.0.0", 229 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 230 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 231 | }, 232 | "node_modules/get-stream": { 233 | "version": "5.2.0", 234 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 235 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 236 | "dependencies": { 237 | "pump": "^3.0.0" 238 | }, 239 | "engines": { 240 | "node": ">=8" 241 | }, 242 | "funding": { 243 | "url": "https://github.com/sponsors/sindresorhus" 244 | } 245 | }, 246 | "node_modules/glob": { 247 | "version": "7.2.3", 248 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 249 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 250 | "deprecated": "Glob versions prior to v9 are no longer supported", 251 | "dependencies": { 252 | "fs.realpath": "^1.0.0", 253 | "inflight": "^1.0.4", 254 | "inherits": "2", 255 | "minimatch": "^3.1.1", 256 | "once": "^1.3.0", 257 | "path-is-absolute": "^1.0.0" 258 | }, 259 | "engines": { 260 | "node": "*" 261 | }, 262 | "funding": { 263 | "url": "https://github.com/sponsors/isaacs" 264 | } 265 | }, 266 | "node_modules/https-proxy-agent": { 267 | "version": "5.0.0", 268 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 269 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 270 | "dependencies": { 271 | "agent-base": "6", 272 | "debug": "4" 273 | }, 274 | "engines": { 275 | "node": ">= 6" 276 | } 277 | }, 278 | "node_modules/ieee754": { 279 | "version": "1.2.1", 280 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 281 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 282 | "funding": [ 283 | { 284 | "type": "github", 285 | "url": "https://github.com/sponsors/feross" 286 | }, 287 | { 288 | "type": "patreon", 289 | "url": "https://www.patreon.com/feross" 290 | }, 291 | { 292 | "type": "consulting", 293 | "url": "https://feross.org/support" 294 | } 295 | ] 296 | }, 297 | "node_modules/inflight": { 298 | "version": "1.0.6", 299 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 300 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 301 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 302 | "dependencies": { 303 | "once": "^1.3.0", 304 | "wrappy": "1" 305 | } 306 | }, 307 | "node_modules/inherits": { 308 | "version": "2.0.4", 309 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 310 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 311 | }, 312 | "node_modules/lambdafs": { 313 | "version": "2.1.1", 314 | "resolved": "https://registry.npmjs.org/lambdafs/-/lambdafs-2.1.1.tgz", 315 | "integrity": "sha512-x5k8JcoJWkWLvCVBzrl4pzvkEHSgSBqFjg3Dpsc4AcTMq7oUMym4cL/gRTZ6VM4mUMY+M0dIbQ+V1c1tsqqanQ==", 316 | "bundleDependencies": [ 317 | "tar-fs" 318 | ], 319 | "dependencies": { 320 | "tar-fs": "*" 321 | }, 322 | "bin": { 323 | "lambdafs": "bin/brotli.js" 324 | }, 325 | "engines": { 326 | "node": ">= 10.16" 327 | } 328 | }, 329 | "node_modules/lambdafs/node_modules/base64-js": { 330 | "version": "1.5.1", 331 | "funding": [ 332 | { 333 | "type": "github", 334 | "url": "https://github.com/sponsors/feross" 335 | }, 336 | { 337 | "type": "patreon", 338 | "url": "https://www.patreon.com/feross" 339 | }, 340 | { 341 | "type": "consulting", 342 | "url": "https://feross.org/support" 343 | } 344 | ], 345 | "inBundle": true, 346 | "license": "MIT" 347 | }, 348 | "node_modules/lambdafs/node_modules/bl": { 349 | "version": "4.1.0", 350 | "inBundle": true, 351 | "license": "MIT", 352 | "dependencies": { 353 | "buffer": "^5.5.0", 354 | "inherits": "^2.0.4", 355 | "readable-stream": "^3.4.0" 356 | } 357 | }, 358 | "node_modules/lambdafs/node_modules/bl/node_modules/readable-stream": { 359 | "version": "3.6.0", 360 | "inBundle": true, 361 | "license": "MIT", 362 | "dependencies": { 363 | "inherits": "^2.0.3", 364 | "string_decoder": "^1.1.1", 365 | "util-deprecate": "^1.0.1" 366 | }, 367 | "engines": { 368 | "node": ">= 6" 369 | } 370 | }, 371 | "node_modules/lambdafs/node_modules/buffer": { 372 | "version": "5.7.1", 373 | "funding": [ 374 | { 375 | "type": "github", 376 | "url": "https://github.com/sponsors/feross" 377 | }, 378 | { 379 | "type": "patreon", 380 | "url": "https://www.patreon.com/feross" 381 | }, 382 | { 383 | "type": "consulting", 384 | "url": "https://feross.org/support" 385 | } 386 | ], 387 | "inBundle": true, 388 | "license": "MIT", 389 | "dependencies": { 390 | "base64-js": "^1.3.1", 391 | "ieee754": "^1.1.13" 392 | } 393 | }, 394 | "node_modules/lambdafs/node_modules/chownr": { 395 | "version": "1.1.4", 396 | "inBundle": true, 397 | "license": "ISC" 398 | }, 399 | "node_modules/lambdafs/node_modules/end-of-stream": { 400 | "version": "1.4.4", 401 | "inBundle": true, 402 | "license": "MIT", 403 | "dependencies": { 404 | "once": "^1.4.0" 405 | } 406 | }, 407 | "node_modules/lambdafs/node_modules/fs-constants": { 408 | "version": "1.0.0", 409 | "inBundle": true, 410 | "license": "MIT" 411 | }, 412 | "node_modules/lambdafs/node_modules/ieee754": { 413 | "version": "1.2.1", 414 | "funding": [ 415 | { 416 | "type": "github", 417 | "url": "https://github.com/sponsors/feross" 418 | }, 419 | { 420 | "type": "patreon", 421 | "url": "https://www.patreon.com/feross" 422 | }, 423 | { 424 | "type": "consulting", 425 | "url": "https://feross.org/support" 426 | } 427 | ], 428 | "inBundle": true, 429 | "license": "BSD-3-Clause" 430 | }, 431 | "node_modules/lambdafs/node_modules/inherits": { 432 | "version": "2.0.4", 433 | "inBundle": true, 434 | "license": "ISC" 435 | }, 436 | "node_modules/lambdafs/node_modules/mkdirp-classic": { 437 | "version": "0.5.3", 438 | "inBundle": true, 439 | "license": "MIT" 440 | }, 441 | "node_modules/lambdafs/node_modules/once": { 442 | "version": "1.4.0", 443 | "inBundle": true, 444 | "license": "ISC", 445 | "dependencies": { 446 | "wrappy": "1" 447 | } 448 | }, 449 | "node_modules/lambdafs/node_modules/pump": { 450 | "version": "3.0.0", 451 | "inBundle": true, 452 | "license": "MIT", 453 | "dependencies": { 454 | "end-of-stream": "^1.1.0", 455 | "once": "^1.3.1" 456 | } 457 | }, 458 | "node_modules/lambdafs/node_modules/string_decoder": { 459 | "version": "1.1.1", 460 | "inBundle": true, 461 | "license": "MIT", 462 | "dependencies": { 463 | "safe-buffer": "~5.1.0" 464 | } 465 | }, 466 | "node_modules/lambdafs/node_modules/string_decoder/node_modules/safe-buffer": { 467 | "version": "5.1.2", 468 | "inBundle": true, 469 | "license": "MIT" 470 | }, 471 | "node_modules/lambdafs/node_modules/tar-fs": { 472 | "version": "2.1.1", 473 | "inBundle": true, 474 | "license": "MIT", 475 | "dependencies": { 476 | "chownr": "^1.1.1", 477 | "mkdirp-classic": "^0.5.2", 478 | "pump": "^3.0.0", 479 | "tar-stream": "^2.1.4" 480 | } 481 | }, 482 | "node_modules/lambdafs/node_modules/tar-stream": { 483 | "version": "2.2.0", 484 | "inBundle": true, 485 | "license": "MIT", 486 | "dependencies": { 487 | "bl": "^4.0.3", 488 | "end-of-stream": "^1.4.1", 489 | "fs-constants": "^1.0.0", 490 | "inherits": "^2.0.3", 491 | "readable-stream": "^3.1.1" 492 | }, 493 | "engines": { 494 | "node": ">=6" 495 | } 496 | }, 497 | "node_modules/lambdafs/node_modules/tar-stream/node_modules/readable-stream": { 498 | "version": "3.6.0", 499 | "inBundle": true, 500 | "license": "MIT", 501 | "dependencies": { 502 | "inherits": "^2.0.3", 503 | "string_decoder": "^1.1.1", 504 | "util-deprecate": "^1.0.1" 505 | }, 506 | "engines": { 507 | "node": ">= 6" 508 | } 509 | }, 510 | "node_modules/lambdafs/node_modules/util-deprecate": { 511 | "version": "1.0.2", 512 | "inBundle": true, 513 | "license": "MIT" 514 | }, 515 | "node_modules/lambdafs/node_modules/wrappy": { 516 | "version": "1.0.2", 517 | "inBundle": true, 518 | "license": "ISC" 519 | }, 520 | "node_modules/locate-path": { 521 | "version": "5.0.0", 522 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 523 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 524 | "dependencies": { 525 | "p-locate": "^4.1.0" 526 | }, 527 | "engines": { 528 | "node": ">=8" 529 | } 530 | }, 531 | "node_modules/minimatch": { 532 | "version": "3.1.2", 533 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 534 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 535 | "dependencies": { 536 | "brace-expansion": "^1.1.7" 537 | }, 538 | "engines": { 539 | "node": "*" 540 | } 541 | }, 542 | "node_modules/minimist": { 543 | "version": "1.2.8", 544 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 545 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 546 | "funding": { 547 | "url": "https://github.com/sponsors/ljharb" 548 | } 549 | }, 550 | "node_modules/mkdirp": { 551 | "version": "0.5.6", 552 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 553 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 554 | "dependencies": { 555 | "minimist": "^1.2.6" 556 | }, 557 | "bin": { 558 | "mkdirp": "bin/cmd.js" 559 | } 560 | }, 561 | "node_modules/ms": { 562 | "version": "2.1.2", 563 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 564 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 565 | }, 566 | "node_modules/node-fetch": { 567 | "version": "2.6.1", 568 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 569 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 570 | "engines": { 571 | "node": "4.x || >=6.0.0" 572 | } 573 | }, 574 | "node_modules/once": { 575 | "version": "1.4.0", 576 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 577 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 578 | "dependencies": { 579 | "wrappy": "1" 580 | } 581 | }, 582 | "node_modules/p-limit": { 583 | "version": "2.3.0", 584 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 585 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 586 | "dependencies": { 587 | "p-try": "^2.0.0" 588 | }, 589 | "engines": { 590 | "node": ">=6" 591 | }, 592 | "funding": { 593 | "url": "https://github.com/sponsors/sindresorhus" 594 | } 595 | }, 596 | "node_modules/p-locate": { 597 | "version": "4.1.0", 598 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 599 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 600 | "dependencies": { 601 | "p-limit": "^2.2.0" 602 | }, 603 | "engines": { 604 | "node": ">=8" 605 | } 606 | }, 607 | "node_modules/p-try": { 608 | "version": "2.2.0", 609 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 610 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 611 | "engines": { 612 | "node": ">=6" 613 | } 614 | }, 615 | "node_modules/path-exists": { 616 | "version": "4.0.0", 617 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 618 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 619 | "engines": { 620 | "node": ">=8" 621 | } 622 | }, 623 | "node_modules/path-is-absolute": { 624 | "version": "1.0.1", 625 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 626 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 627 | "engines": { 628 | "node": ">=0.10.0" 629 | } 630 | }, 631 | "node_modules/pend": { 632 | "version": "1.2.0", 633 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 634 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" 635 | }, 636 | "node_modules/pkg-dir": { 637 | "version": "4.2.0", 638 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 639 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 640 | "dependencies": { 641 | "find-up": "^4.0.0" 642 | }, 643 | "engines": { 644 | "node": ">=8" 645 | } 646 | }, 647 | "node_modules/progress": { 648 | "version": "2.0.1", 649 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", 650 | "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", 651 | "engines": { 652 | "node": ">=0.4.0" 653 | } 654 | }, 655 | "node_modules/proxy-from-env": { 656 | "version": "1.1.0", 657 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 658 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 659 | }, 660 | "node_modules/pump": { 661 | "version": "3.0.0", 662 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 663 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 664 | "dependencies": { 665 | "end-of-stream": "^1.1.0", 666 | "once": "^1.3.1" 667 | } 668 | }, 669 | "node_modules/puppeteer-core": { 670 | "version": "10.1.0", 671 | "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-10.1.0.tgz", 672 | "integrity": "sha512-x2yDSJI/PRiWhDqAt1jd4rhTotxwjwKzHLIIqD2MlJ+TmzGJfBY9snAGIVXJwkWfKJg+Ef5xupdK0EbHDqBpFw==", 673 | "dependencies": { 674 | "debug": "4.3.1", 675 | "devtools-protocol": "0.0.883894", 676 | "extract-zip": "2.0.1", 677 | "https-proxy-agent": "5.0.0", 678 | "node-fetch": "2.6.1", 679 | "pkg-dir": "4.2.0", 680 | "progress": "2.0.1", 681 | "proxy-from-env": "1.1.0", 682 | "rimraf": "3.0.2", 683 | "tar-fs": "2.0.0", 684 | "unbzip2-stream": "1.3.3", 685 | "ws": "7.4.6" 686 | }, 687 | "engines": { 688 | "node": ">=10.18.1" 689 | } 690 | }, 691 | "node_modules/readable-stream": { 692 | "version": "3.6.2", 693 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 694 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 695 | "dependencies": { 696 | "inherits": "^2.0.3", 697 | "string_decoder": "^1.1.1", 698 | "util-deprecate": "^1.0.1" 699 | }, 700 | "engines": { 701 | "node": ">= 6" 702 | } 703 | }, 704 | "node_modules/rimraf": { 705 | "version": "3.0.2", 706 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 707 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 708 | "deprecated": "Rimraf versions prior to v4 are no longer supported", 709 | "dependencies": { 710 | "glob": "^7.1.3" 711 | }, 712 | "bin": { 713 | "rimraf": "bin.js" 714 | }, 715 | "funding": { 716 | "url": "https://github.com/sponsors/isaacs" 717 | } 718 | }, 719 | "node_modules/safe-buffer": { 720 | "version": "5.2.1", 721 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 722 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 723 | "funding": [ 724 | { 725 | "type": "github", 726 | "url": "https://github.com/sponsors/feross" 727 | }, 728 | { 729 | "type": "patreon", 730 | "url": "https://www.patreon.com/feross" 731 | }, 732 | { 733 | "type": "consulting", 734 | "url": "https://feross.org/support" 735 | } 736 | ] 737 | }, 738 | "node_modules/string_decoder": { 739 | "version": "1.3.0", 740 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 741 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 742 | "dependencies": { 743 | "safe-buffer": "~5.2.0" 744 | } 745 | }, 746 | "node_modules/tar-fs": { 747 | "version": "2.0.0", 748 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", 749 | "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", 750 | "dependencies": { 751 | "chownr": "^1.1.1", 752 | "mkdirp": "^0.5.1", 753 | "pump": "^3.0.0", 754 | "tar-stream": "^2.0.0" 755 | } 756 | }, 757 | "node_modules/tar-stream": { 758 | "version": "2.2.0", 759 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 760 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 761 | "dependencies": { 762 | "bl": "^4.0.3", 763 | "end-of-stream": "^1.4.1", 764 | "fs-constants": "^1.0.0", 765 | "inherits": "^2.0.3", 766 | "readable-stream": "^3.1.1" 767 | }, 768 | "engines": { 769 | "node": ">=6" 770 | } 771 | }, 772 | "node_modules/through": { 773 | "version": "2.3.8", 774 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 775 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 776 | }, 777 | "node_modules/unbzip2-stream": { 778 | "version": "1.3.3", 779 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", 780 | "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", 781 | "dependencies": { 782 | "buffer": "^5.2.1", 783 | "through": "^2.3.8" 784 | } 785 | }, 786 | "node_modules/undici-types": { 787 | "version": "6.19.8", 788 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 789 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 790 | "optional": true 791 | }, 792 | "node_modules/util-deprecate": { 793 | "version": "1.0.2", 794 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 795 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 796 | }, 797 | "node_modules/wrappy": { 798 | "version": "1.0.2", 799 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 800 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 801 | }, 802 | "node_modules/ws": { 803 | "version": "7.4.6", 804 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 805 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", 806 | "engines": { 807 | "node": ">=8.3.0" 808 | }, 809 | "peerDependencies": { 810 | "bufferutil": "^4.0.1", 811 | "utf-8-validate": "^5.0.2" 812 | }, 813 | "peerDependenciesMeta": { 814 | "bufferutil": { 815 | "optional": true 816 | }, 817 | "utf-8-validate": { 818 | "optional": true 819 | } 820 | } 821 | }, 822 | "node_modules/yauzl": { 823 | "version": "2.10.0", 824 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 825 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 826 | "dependencies": { 827 | "buffer-crc32": "~0.2.3", 828 | "fd-slicer": "~1.1.0" 829 | } 830 | } 831 | } 832 | } 833 | ` 834 | 835 | const PACKAGE_JSON = `{ 836 | "name": "browser-test", 837 | "version": "1.0.0", 838 | "main": "index.js", 839 | "scripts": { 840 | "dev": "next dev", 841 | "build": "next build", 842 | "start": "next start", 843 | "lint": "next lint" 844 | }, 845 | "keywords": [], 846 | "author": "", 847 | "license": "ISC", 848 | "description": "", 849 | "dependencies": { 850 | "@sparticuz/chromium": "^123.0.0", 851 | "next": "14.1.4", 852 | "puppeteer": "^22.6.2", 853 | "puppeteer-core": "^22.6.2", 854 | "react": "^18", 855 | "react-dom": "^18" 856 | }, 857 | "devDependencies": { 858 | "@types/node": "^20", 859 | "@types/react": "^18", 860 | "@types/react-dom": "^18", 861 | "autoprefixer": "^10.0.1", 862 | "eslint": "^8", 863 | "eslint-config-next": "14.1.4", 864 | "postcss": "^8", 865 | "tailwindcss": "^3.3.0", 866 | "typescript": "^5" 867 | } 868 | } 869 | ` 870 | 871 | const INDEX_HTML = `Click me` 872 | --------------------------------------------------------------------------------