cmd/pkgsite: add command

Functionality for running the pkgsite frontend locally is moved from
cmd/frontend to cmd/pkgsite, since cmd/frontend is currently overloaded
with flag options and running locally does not need all the dependencies
for running cmd/frontend.

Additional functionality will be added to cmd/pkgsite in future CLs.

For golang/go#40371

Change-Id: I4230aa9539c94e01a68eda33cc6492ae377debff
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/290134
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Trust: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 7ff5d7d..953d4fe 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -10,7 +10,6 @@
 	"flag"
 	"net/http"
 	"os"
-	"path/filepath"
 	"time"
 
 	"cloud.google.com/go/profiler"
@@ -21,7 +20,6 @@
 	"golang.org/x/pkgsite/internal/config"
 	"golang.org/x/pkgsite/internal/dcensus"
 	"golang.org/x/pkgsite/internal/frontend"
-	"golang.org/x/pkgsite/internal/localdatasource"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/middleware"
 	"golang.org/x/pkgsite/internal/postgres"
@@ -42,8 +40,6 @@
 		"for direct proxy mode and frontend fetches")
 	directProxy = flag.Bool("direct_proxy", false, "if set to true, uses the module proxy referred to by this URL "+
 		"as a direct backend, bypassing the database")
-	localPaths         = flag.String("local", "", "run locally, accepts a GOPATH-like collection of local paths for modules to load to memory")
-	gopathMode         = flag.Bool("gopath_mode", false, "assume that local modules' paths are relative to GOPATH/src, used only with -local")
 	bypassLicenseCheck = flag.Bool("bypass_license_check", false, "display all information, even for non-redistributable paths")
 )
 
@@ -75,41 +71,36 @@
 	expg := cmdconfig.ExperimentGetter(ctx, cfg)
 	log.Infof(ctx, "cmd/frontend: initialized cmdconfig.ExperimentGetter")
 
-	if *localPaths != "" {
-		lds := localdatasource.New()
-		dsg = func(context.Context) internal.DataSource { return lds }
-	} else {
-		proxyClient, err := proxy.New(*proxyURL)
-		if err != nil {
-			log.Fatal(ctx, err)
-		}
+	proxyClient, err := proxy.New(*proxyURL)
+	if err != nil {
+		log.Fatal(ctx, err)
+	}
 
-		if *directProxy {
-			var pds *proxydatasource.DataSource
-			if *bypassLicenseCheck {
-				pds = proxydatasource.NewBypassingLicenseCheck(proxyClient)
-			} else {
-				pds = proxydatasource.New(proxyClient)
-			}
-			dsg = func(context.Context) internal.DataSource { return pds }
+	if *directProxy {
+		var pds *proxydatasource.DataSource
+		if *bypassLicenseCheck {
+			pds = proxydatasource.NewBypassingLicenseCheck(proxyClient)
 		} else {
-			db, err := cmdconfig.OpenDB(ctx, cfg, *bypassLicenseCheck)
-			if err != nil {
-				log.Fatalf(ctx, "%v", err)
-			}
-			defer db.Close()
-			dsg = func(context.Context) internal.DataSource { return db }
-			sourceClient := source.NewClient(config.SourceTimeout)
-			// The closure passed to queue.New is only used for testing and local
-			// execution, not in production. So it's okay that it doesn't use a
-			// per-request connection.
-			fetchQueue, err = queue.New(ctx, cfg, queueName, *workers, expg,
-				func(ctx context.Context, modulePath, version string) (int, error) {
-					return frontend.FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, db)
-				})
-			if err != nil {
-				log.Fatalf(ctx, "queue.New: %v", err)
-			}
+			pds = proxydatasource.New(proxyClient)
+		}
+		dsg = func(context.Context) internal.DataSource { return pds }
+	} else {
+		db, err := cmdconfig.OpenDB(ctx, cfg, *bypassLicenseCheck)
+		if err != nil {
+			log.Fatalf(ctx, "%v", err)
+		}
+		defer db.Close()
+		dsg = func(context.Context) internal.DataSource { return db }
+		sourceClient := source.NewClient(config.SourceTimeout)
+		// The closure passed to queue.New is only used for testing and local
+		// execution, not in production. So it's okay that it doesn't use a
+		// per-request connection.
+		fetchQueue, err = queue.New(ctx, cfg, queueName, *workers, expg,
+			func(ctx context.Context, modulePath, version string) (int, error) {
+				return frontend.FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, db)
+			})
+		if err != nil {
+			log.Fatalf(ctx, "queue.New: %v", err)
 		}
 	}
 
@@ -135,13 +126,6 @@
 		log.Fatalf(ctx, "frontend.NewServer: %v", err)
 	}
 
-	if *localPaths != "" {
-		lds, ok := dsg(ctx).(*localdatasource.DataSource)
-		if ok {
-			load(ctx, lds, *localPaths)
-		}
-	}
-
 	router := dcensus.NewRouter(frontend.TagRoute)
 	var cacheClient *redis.Client
 	if cfg.RedisCacheHost != "" {
@@ -204,25 +188,3 @@
 	log.Infof(ctx, "Listening on addr %s", addr)
 	log.Fatal(ctx, http.ListenAndServe(addr, mw(router)))
 }
-
-// load loads local modules from pathList.
-func load(ctx context.Context, ds *localdatasource.DataSource, pathList string) {
-	paths := filepath.SplitList(pathList)
-	loaded := len(paths)
-	for _, path := range paths {
-		var err error
-		if *gopathMode {
-			err = ds.LoadFromGOPATH(ctx, path)
-		} else {
-			err = ds.Load(ctx, path)
-		}
-		if err != nil {
-			log.Error(ctx, err)
-			loaded--
-		}
-	}
-
-	if loaded == 0 {
-		log.Fatalf(ctx, "failed to load module(s) at %s", pathList)
-	}
-}
diff --git a/cmd/pkgsite/main.go b/cmd/pkgsite/main.go
new file mode 100644
index 0000000..bfb1b0a
--- /dev/null
+++ b/cmd/pkgsite/main.go
@@ -0,0 +1,99 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is a work in progress.
+//
+// Pkgsite extracts and generates documentation for Go programs.
+// It runs as a web server and presents the documentation as a
+// web page.
+// Usage:
+//
+//  pkgsite [flag]
+//
+// The flags are:
+//
+//  -local=path1,path2
+//      Accepts a GOPATH-like collection of local paths for modules to load to memory
+//  -gopath_mode=false
+//      Assume that local modules' paths are relative to GOPATH/src
+package main
+
+import (
+	"context"
+	"flag"
+	"net/http"
+	"path/filepath"
+	"time"
+
+	"github.com/google/safehtml/template"
+	"golang.org/x/pkgsite/internal"
+	"golang.org/x/pkgsite/internal/dcensus"
+	"golang.org/x/pkgsite/internal/frontend"
+	"golang.org/x/pkgsite/internal/localdatasource"
+	"golang.org/x/pkgsite/internal/log"
+	"golang.org/x/pkgsite/internal/middleware"
+)
+
+var (
+	_          = flag.String("static", "content/static", "path to folder containing static files served")
+	localPaths = flag.String("local", "", "run locally, accepts a GOPATH-like collection of local paths for modules to load to memory")
+	gopathMode = flag.Bool("gopath_mode", false, "assume that local modules' paths are relative to GOPATH/src, used only with -local")
+)
+
+func main() {
+	flag.Parse()
+	ctx := context.Background()
+	var dsg func(context.Context) internal.DataSource
+	if *localPaths == "" {
+		log.Fatalf(ctx, "-local is not set")
+	}
+
+	lds := localdatasource.New()
+	dsg = func(context.Context) internal.DataSource { return lds }
+	server, err := frontend.NewServer(frontend.ServerConfig{
+		DataSourceGetter: dsg,
+		StaticPath:       template.TrustedSourceFromFlag(flag.Lookup("static").Value),
+	})
+	if err != nil {
+		log.Fatalf(ctx, "frontend.NewServer: %v", err)
+	}
+	lds, ok := dsg(ctx).(*localdatasource.DataSource)
+	if ok {
+		load(ctx, lds, *localPaths)
+	}
+
+	router := dcensus.NewRouter(frontend.TagRoute)
+	server.Install(router.Handle, nil, nil)
+
+	mw := middleware.Chain(
+		middleware.RedirectedFrom(),
+		middleware.LatestVersions(server.GetLatestInfo), // must come before caching for version badge to work
+		middleware.Timeout(54*time.Second),
+	)
+	addr := "localhost:6060"
+	log.Infof(ctx, "Listening on addr %s", addr)
+	log.Fatal(ctx, http.ListenAndServe(addr, mw(router)))
+}
+
+// load loads local modules from pathList.
+func load(ctx context.Context, ds *localdatasource.DataSource, pathList string) {
+	paths := filepath.SplitList(pathList)
+	loaded := len(paths)
+	for _, path := range paths {
+		var err error
+		if *gopathMode {
+			err = ds.LoadFromGOPATH(ctx, path)
+		} else {
+			err = ds.Load(ctx, path)
+		}
+		if err != nil {
+			log.Error(ctx, err)
+			loaded--
+		}
+	}
+
+	if loaded == 0 {
+		log.Fatalf(ctx, "failed to load module(s) at %s", pathList)
+	}
+}
diff --git a/doc/frontend.md b/doc/frontend.md
index 8cf070a..3f38d31 100644
--- a/doc/frontend.md
+++ b/doc/frontend.md
@@ -16,7 +16,7 @@
 
 You can run the frontend locally like so:
 
-    go run ./cmd/frontend [-dev] [-direct_proxy] [-local .]
+    go run ./cmd/frontend [-dev] [-direct_proxy]
 
 - The `-dev` flag reloads templates on each page load.
 
@@ -24,7 +24,6 @@
 
 - Postgres database
 - Proxy service
-- Local filesystem
 
 The `Datasource` interface implementation is available at internal/datasource.go.
 
@@ -39,16 +38,21 @@
 
 You can then run the frontend with: `go run ./cmd/frontend`
 
-You can also use `-local` flag to run the frontend with an in-memory datasource
-populated with modules loaded from your local filesystem. This allows you to run
-the frontend without setting up a database and to view documentation of local
-modules without requiring a proxy. `-local` accepts a GOPATH-like string containing
-paths of modules to load into memory.
-
 If you add, change or remove any inline scripts in templates, run
 `devtools/cmd/csphash` to update the hashes. Running `all.bash`
 will do that as well.
 
+### Local mode
+
+You can also use run the frontend locally with an in-memory datasource
+populated with modules loaded from your local filesystem.
+
+    go run ./cmd/pkgsite [-local .]
+
+This allows you to run the frontend without setting up a database and to view
+documentation of local modules without requiring a proxy. `-local` accepts a
+GOPATH-like string containing paths of modules to load into memory.
+
 ### Testing
 
 In addition to tests inside internal/frontend and internal/testing/integration,