Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions __tests__/bin/vip-edge-workers-get.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ describe( 'edgeWorkersGetCommand()', () => {
expect( console.log ).toHaveBeenCalledWith( 'export default {};' );
} );

it.each( [
[
'newlines and tabs',
'// café\n\nexport function run(): void {\n\treturn;\n}\n',
'// café\n\nexport function run(): void {\n\treturn;\n}\n',
],
[ 'Windows line endings', 'export {};\r\n\t// comment\r\n', 'export {};\n\t// comment\n' ],
[
'terminal controls',
'\u0000\u0007\b\v\f\r\u001b[2J\u007f\u0085\u009b31m',
String.raw`\u0000\u0007\u0008\u000b\u000c\u000d\u001b[2J\u007f\u0085\u009b31m`,
],
[
'literal escape sequences',
String.raw`// literal \u000a and \t`,
String.raw`// literal \u000a and \t`,
],
] )( 'renders stored source with safe %s', async ( _name, source, expected ) => {
edgeWorkersApi.getEdgeWorker.mockResolvedValue( { ...worker, source } );

await edgeWorkersGetCommand( [ 'headers' ], { ...opts, source: true } );

expect( console.log ).toHaveBeenLastCalledWith( expected );
} );

it( 'reports when explicitly requested source was not stored', async () => {
edgeWorkersApi.getEdgeWorker.mockResolvedValue( { ...worker, source: null } );

Expand Down
1 change: 1 addition & 0 deletions __tests__/bin/vip-edge-workers-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe( 'edgeWorkersInitCommand()', () => {
expect( console.log ).toHaveBeenCalledWith(
expect.stringContaining( 'Created a new assemblyscript edge-workers project' )
);
expect( console.log ).toHaveBeenCalledWith( ' vip edge-workers new my-worker' );
const scaffoldOrder = scaffoldProject.mock.invocationCallOrder[ 0 ];
const successOrder = tracker.trackEvent.mock.invocationCallOrder.at( -1 );
expect( scaffoldOrder ).toBeLessThan( successOrder );
Expand Down
13 changes: 13 additions & 0 deletions __tests__/lib/edge-workers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,19 @@ describe( 'edge-workers project', () => {
} );

describe( 'worker source and artifacts', () => {
it.each( [
[ [ 0xff, 0xff ], '\ufffd\ufffd' ],
[ [ 0xe1, 0x80 ], '\ufffd' ],
[ [ 0xe1, 0x80, 0xff ], '\ufffd\ufffd' ],
] )( 'replaces malformed UTF-8 source sequences %j', ( bytes, expected ) => {
const worker = {
dir: tmp,
manifest: { name: 'headers', entry: 'source.ts' },
};
fs.writeFileSync( path.join( tmp, 'source.ts' ), Buffer.from( bytes ) );
expect( readWorkerSource( worker ) ).toBe( expected );
} );

it( 'throws when worker source cannot be read', () => {
const worker = {
dir: path.join( tmp, 'worker' ),
Expand Down
197 changes: 197 additions & 0 deletions cmd/vip-next/commands/edge_workers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package commands

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/Automattic/vip/internal/appctx"
"github.com/Automattic/vip/internal/edgeworkers"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/term"
)

type edgeWorkersDeps struct {
Getwd func() (string, error)
Service edgeworkers.Service
Confirm func(*cobra.Command, string, bool) (bool, error)
IsInteractive func(*cobra.Command) bool
StdoutTTY func(*cobra.Command) bool
}

func NewEdgeWorkersCmd() *cobra.Command {
return newEdgeWorkersCmd(edgeWorkersDeps{Service: edgeworkers.Service{API: edgeworkers.APIClient{Client: GetConfig().GQLClient}, Builder: edgeworkers.Compiler{}}})
}
func newEdgeWorkersCmd(deps edgeWorkersDeps) *cobra.Command {
if deps.Getwd == nil {
deps.Getwd = os.Getwd
}
if deps.Confirm == nil {
deps.Confirm = appctx.Confirm
}
if deps.IsInteractive == nil {
deps.IsInteractive = appctx.IsInteractive
}
if deps.StdoutTTY == nil {
deps.StdoutTTY = func(c *cobra.Command) bool {
f, ok := c.OutOrStdout().(*os.File)
return ok && term.IsTerminal(int(f.Fd()))
}
}
c := &cobra.Command{Use: "edge-workers", Short: "Manage edge workers for an environment.", RunE: func(c *cobra.Command, _ []string) error { return c.Help() }}
// NUL distinguishes a bare optional flag from the literal string "true".
// It is parser state, not a display default; do not let pflag emit it.
defaultHelp := c.HelpFunc()
c.SetHelpFunc(func(cmd *cobra.Command, args []string) {
var marked []*pflag.Flag
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
if flag.NoOptDefVal == edgeWorkersBareValue {
marked = append(marked, flag)
flag.NoOptDefVal = ""
}
})
defer func() {
for _, flag := range marked {
flag.NoOptDefVal = edgeWorkersBareValue
}
}()
defaultHelp(cmd, args)
})
c.AddCommand(newEdgeWorkersInitCmd(deps), newEdgeWorkersNewCmd(deps), newEdgeWorkersBuildCmd(deps), newEdgeWorkersListCmd(deps), newEdgeWorkersGetCmd(deps), newEdgeWorkersValidateCmd(deps), newEdgeWorkersDeployCmd(deps), newEdgeWorkersLifecycleCmd(deps, "enable"), newEdgeWorkersLifecycleCmd(deps, "disable"), newEdgeWorkersLifecycleCmd(deps, "delete"))
return c
}
func edgeWorkersTrack(c *cobra.Command, event string, props map[string]any) {
p := map[string]any{}
for k, v := range props {
p[k] = v
}
if ae := appctx.FromContext(c.Context()); ae != nil {
p["app_id"] = ae.App.ID
p["env_id"] = ae.Env.ID
}
trackEvent("edge_workers_"+event, p)
}
func edgeName(args []string) string {
if len(args) > 0 {
return args[0]
}
return ""
}
func edgeNameProps(args []string) map[string]any {
p := map[string]any{}
if len(args) > 0 {
p["name"] = args[0]
}
return p
}
func edgeBool(c *cobra.Command, name string) bool { v, _ := c.Flags().GetBool(name); return v }
func edgeError(c *cobra.Command, action string, args []string, err error) error {
var apiErr *edgeworkers.APIError
if errors.As(err, &apiErr) {
for _, message := range apiErr.Messages {
fmt.Fprintf(c.ErrOrStderr(), "Error: %s\n", edgeworkers.EscapeTerminalText(message))
}
}
p := edgeNameProps(args)
p["error"] = action + "_failed"
edgeWorkersTrack(c, action+"_command_error", p)
var partial *edgeworkers.ApplyError
if errors.As(err, &partial) {
return errors.New(edgeworkers.PartialFailureMessage(partial))
}
message := edgeworkers.EscapeTerminalText(err.Error())
if action == "init" || action == "new" || action == "build" {
return errors.New(message)
}
noun := "edge worker"
if action == "list" {
noun = "edge workers"
}
return fmt.Errorf("Failed to %s %s: %s", action, noun, message)
}
func edgeProject(deps edgeWorkersDeps, path *edgeWorkersStringFlag) (string, error) {
cwd, err := deps.Getwd()
if err != nil {
return "", err
}
var explicit *string
if len(path.Values) > 0 {
v, ok := edgeWorkersOptionValue(path).(string)
if !ok {
return "", errors.New("The --path flag requires a path to the edge-workers project.")
}
explicit = &v
}
return edgeworkers.ResolveProjectDir(cwd, explicit)
}
func edgeSelect(deps edgeWorkersDeps, c *cobra.Command, args []string, path *edgeWorkersStringFlag, action string) (string, []edgeworkers.LocalWorker, error) {
name := edgeName(args)
all := edgeBool(c, "all")
if name != "" && all {
return "", nil, errors.New("Supply either a worker name or --all, not both.")
}
dir, err := edgeProject(deps, path)
if err != nil {
return "", nil, err
}
if name != "" {
w, err := edgeworkers.FindWorker(dir, name)
if err != nil {
return "", nil, err
}
return dir, []edgeworkers.LocalWorker{w}, nil
}
if !all && action != "build" {
return "", nil, fmt.Errorf("Please supply a worker name to %s, or pass `--all`.", action)
}
workers, err := edgeworkers.DiscoverWorkers(dir)
if err != nil {
return "", nil, err
}
if len(workers) == 0 {
message := "No workers found in this project."
if action == "build" {
message += " Create one with `vip edge-workers new`."
}
return "", nil, errors.New(message)
}
return dir, workers, nil
}
func edgeRemote(c *cobra.Command, required bool, handler appctx.RunFunc) *cobra.Command {
addAppEnvFlags(c)
mw := []appctx.Middleware{appctx.WithAppContext(GetConfig().AppCtxConfig), appctx.WithEnvContext()}
if required {
mw = append(mw, appctx.WithRequiredArgs(1))
}
return appctx.Build(c, mw...).WithRun(handler)
}
func edgeConfirm(deps edgeWorkersDeps, c *cobra.Command) func(string) (bool, error) {
return func(message string) (bool, error) {
ok, err := deps.Confirm(c, message, false)
if errors.Is(err, appctx.ErrNonInteractive) {
return false, nil
}
return ok, err
}
}
func edgeProduction(deps edgeWorkersDeps, c *cobra.Command, action string, names []string, enable bool) error {
ae := appctx.FromContext(c.Context())
return edgeworkers.ConfirmProduction(edgeworkers.ProductionConfirmation{Action: action, AppName: ae.App.Name, EnvType: ae.Env.Type, WorkerNames: names, EnableAfterDeploy: enable, SkipConfirmation: edgeBool(c, "skip-confirmation"), NonInteractive: !deps.IsInteractive(c) || !deps.StdoutTTY(c)}, edgeConfirm(deps, c))
}
func edgeArtifact(deps edgeWorkersDeps, c *cobra.Command, dir string, w edgeworkers.LocalWorker) (edgeworkers.Artifact, error) {
if edgeBool(c, "skip-build") {
return edgeworkers.ReadPrebuilt(dir, w)
}
if deps.Service.Builder == nil {
return edgeworkers.Artifact{}, errors.New("No worker compiler configured.")
}
return deps.Service.Builder.Build(c.Context(), dir, w)
}
func edgeAbs(cwd, target string) string {
if filepath.IsAbs(target) {
return filepath.Clean(target)
}
return filepath.Join(cwd, target)
}
56 changes: 56 additions & 0 deletions cmd/vip-next/commands/edge_workers_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package commands

import (
"fmt"
"github.com/Automattic/vip/internal/nodeflags"
"github.com/spf13/cobra"
"strings"
)

const edgeWorkersBareValue = "\x00"

type edgeWorkersStringFlag struct{ Values []string }

func (v *edgeWorkersStringFlag) String() string {
if len(v.Values) == 0 {
return ""
}
return v.Values[len(v.Values)-1]
}
func (*edgeWorkersStringFlag) Type() string { return "string" }
func (v *edgeWorkersStringFlag) Set(s string) error { v.Values = append(v.Values, s); return nil }
func edgeWorkersOptionValue(v *edgeWorkersStringFlag) any {
if len(v.Values) == 0 {
return nil
}
values := make([]any, len(v.Values))
for i, s := range v.Values {
values[i] = s
if s == edgeWorkersBareValue {
values[i] = true
}
}
if len(values) == 1 {
return values[0]
}
return values
}
func edgeOptionText(value any) string {
if values, ok := value.([]any); ok {
parts := make([]string, len(values))
for i, v := range values {
parts[i] = fmt.Sprint(v)
}
return strings.Join(parts, ",")
}
return fmt.Sprint(value)
}
func edgeStringFlag(c *cobra.Command, name, short, description string) *edgeWorkersStringFlag {
v := &edgeWorkersStringFlag{}
c.Flags().VarP(v, name, short, description)
nodeflags.MarkOptionalValue(c, edgeWorkersBareValue, name)
return v
}
func edgePathFlag(c *cobra.Command) *edgeWorkersStringFlag {
return edgeStringFlag(c, "path", "p", "Path to the edge-workers project. Defaults to auto-discovery.")
}
Loading
Loading