feat(14.3-01): detect the framework checkout and guard summer new targets
- resolve the framework from --framework, then a sibling summercms or summercms.go checkout, then the binary's own source tree, each accepted only when its go.mod declares the framework module - refuse a missing parent and a target inside the framework checkout - remove the half-written app when any step fails - add --no-plugin for an app with no starter plugin
This commit is contained in:
@@ -309,7 +309,8 @@ func newCommand() bonfire.Command {
|
||||
Required: true,
|
||||
}},
|
||||
Flags: []bonfire.Flag{
|
||||
{Name: "framework", Description: "Path to the framework checkout"},
|
||||
{Name: "framework", Description: "Path to the framework checkout (default: a sibling summercms or summercms.go checkout)"},
|
||||
{Name: "no-plugin", Description: "Do not create the starter plugin", Bare: true},
|
||||
},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
name, ok := in.Argument("name")
|
||||
@@ -325,6 +326,7 @@ func newCommand() bonfire.Command {
|
||||
Name: name,
|
||||
Parent: dir,
|
||||
Framework: framework,
|
||||
NoPlugin: flagTrue(in, "no-plugin"),
|
||||
}, out)
|
||||
return err
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// appNameRe is the public `summer new <name>` rule: lower-case letters and
|
||||
@@ -24,10 +26,23 @@ type NewOptions struct {
|
||||
// current directory.
|
||||
Parent string
|
||||
// Framework is the path to the framework checkout, resolved against
|
||||
// Parent when relative.
|
||||
// Parent when relative. Empty means auto-detection: a sibling
|
||||
// summercms or summercms.go checkout, then the source tree this binary
|
||||
// was built from.
|
||||
Framework string
|
||||
// NoPlugin skips the starter plugin; summer.yaml then lists no plugins.
|
||||
NoPlugin bool
|
||||
}
|
||||
|
||||
// frameworkSiblings are the directory names auto-detection tries next to
|
||||
// the new application. A candidate counts only when its go.mod declares the
|
||||
// framework module.
|
||||
var frameworkSiblings = []string{"summercms", "summercms.go"}
|
||||
|
||||
// frameworkFromBinary returns the framework checkout this binary was built
|
||||
// from, or "" when unknown. Tests replace it.
|
||||
var frameworkFromBinary = binarySourceFramework
|
||||
|
||||
// genFile is one file NewApp writes itself, relative to the app root.
|
||||
type genFile struct {
|
||||
Rel string
|
||||
@@ -89,23 +104,24 @@ func NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build: resolve parent directory: %w", err)
|
||||
}
|
||||
if st, err := os.Stat(parent); err != nil {
|
||||
return "", fmt.Errorf("build: parent directory: %w", err)
|
||||
} else if !st.IsDir() {
|
||||
return "", fmt.Errorf("build: parent %s is not a directory", parent)
|
||||
}
|
||||
target := filepath.Join(parent, appDirName(opts.Name))
|
||||
if _, err := os.Lstat(target); err == nil {
|
||||
return "", fmt.Errorf("build: %s already exists; choose another name or remove it", target)
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("build: stat %s: %w", target, err)
|
||||
}
|
||||
if opts.Framework == "" {
|
||||
return "", fmt.Errorf("build: framework checkout not found; pass --framework <path>")
|
||||
}
|
||||
framework := opts.Framework
|
||||
if !filepath.IsAbs(framework) {
|
||||
framework = filepath.Join(parent, framework)
|
||||
}
|
||||
framework = filepath.Clean(framework)
|
||||
if err := checkFramework(framework); err != nil {
|
||||
framework, err := resolveFramework(parent, opts.Framework)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if underRoot(framework, target) {
|
||||
return "", fmt.Errorf("build: %s is inside the framework checkout %s; run summer new in the directory next to the checkout", target, framework)
|
||||
}
|
||||
|
||||
files, err := planNewApp(opts, target, framework)
|
||||
if err != nil {
|
||||
@@ -114,21 +130,70 @@ func NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error)
|
||||
if err := os.Mkdir(target, 0o755); err != nil {
|
||||
return "", fmt.Errorf("build: create %s: %w", target, err)
|
||||
}
|
||||
// target did not exist before this run, so a failure removes it and a
|
||||
// re-run with the same name is not blocked.
|
||||
if err := populateApp(ctx, opts, target, files); err != nil {
|
||||
return "", err
|
||||
if rmErr := os.RemoveAll(target); rmErr != nil {
|
||||
return "", fmt.Errorf("build: create %s: %w (removing it failed: %v)", target, err, rmErr)
|
||||
}
|
||||
return "", fmt.Errorf("build: create %s (removed it): %w", target, err)
|
||||
}
|
||||
fmt.Fprintf(out, "created %s\n", target)
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// resolveFramework picks the framework checkout: the explicit flag (never
|
||||
// falling through when it is wrong), then a sibling of parent whose go.mod
|
||||
// declares the framework module, then the source tree of this binary. Paths
|
||||
// stay lexical so a symlinked sibling yields a ../<sibling> replace.
|
||||
func resolveFramework(parent, flag string) (string, error) {
|
||||
if flag != "" {
|
||||
dir := flag
|
||||
if !filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(parent, dir)
|
||||
}
|
||||
dir = filepath.Clean(dir)
|
||||
if err := checkFramework(dir); err != nil {
|
||||
return "", fmt.Errorf("build: --framework %s: %w", flag, err)
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
for _, name := range frameworkSiblings {
|
||||
dir := filepath.Join(parent, name)
|
||||
if checkFramework(dir) == nil {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
if dir := frameworkFromBinary(); dir != "" && checkFramework(dir) == nil {
|
||||
return dir, nil
|
||||
}
|
||||
return "", fmt.Errorf("build: no framework checkout found: looked for %s next to %s; pass --framework <path>",
|
||||
strings.Join(frameworkSiblings, " and "), parent)
|
||||
}
|
||||
|
||||
// binarySourceFramework returns the repository root this file was compiled
|
||||
// from. A -trimpath build records a module-relative path, which is ignored.
|
||||
func binarySourceFramework() string {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok || !filepath.IsAbs(file) {
|
||||
return ""
|
||||
}
|
||||
// file is <root>/internal/build/new.go.
|
||||
root := filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||
if checkFramework(root) != nil {
|
||||
return ""
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// checkFramework requires dir/go.mod to declare the framework module.
|
||||
func checkFramework(dir string) error {
|
||||
module, _, err := parseGoMod(filepath.Join(dir, "go.mod"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build: framework checkout %s: %w", dir, err)
|
||||
return fmt.Errorf("framework checkout %s: %w", dir, err)
|
||||
}
|
||||
if module != frameworkModule {
|
||||
return fmt.Errorf("build: %s declares module %q, not the framework %s", dir, module, frameworkModule)
|
||||
return fmt.Errorf("%s declares module %q, not the framework %s", dir, module, frameworkModule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -141,7 +206,7 @@ func planNewApp(opts NewOptions, target, framework string) ([]genFile, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkFramework(framework); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("build: %w", err)
|
||||
}
|
||||
replace, err := relModulePath(target, framework)
|
||||
if err != nil {
|
||||
@@ -198,11 +263,10 @@ func populateApp(ctx context.Context, opts NewOptions, target string, files []ge
|
||||
if err := SaveManifest(manifestPath, Manifest{Module: appModulePath(opts.Name), Binary: opts.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := makePlugin(ctx, target, "golem15."+opts.Name, pluginOptions{StarterRoute: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := AddPlugin(ctx, target, filepath.Join("plugins", opts.Name)); err != nil {
|
||||
return err
|
||||
if !opts.NoPlugin {
|
||||
if err := addStarterPlugin(ctx, opts.Name, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m, err := LoadManifest(manifestPath)
|
||||
if err != nil {
|
||||
@@ -210,3 +274,12 @@ func populateApp(ctx context.Context, opts NewOptions, target string, files []ge
|
||||
}
|
||||
return generate(target, m)
|
||||
}
|
||||
|
||||
// addStarterPlugin creates golem15.<name> through the make:plugin and
|
||||
// plugin:add code paths, with the starter route.
|
||||
func addStarterPlugin(ctx context.Context, name, target string) error {
|
||||
if _, err := makePlugin(ctx, target, "golem15."+name, pluginOptions{StarterRoute: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
return AddPlugin(ctx, target, filepath.Join("plugins", name))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user