Skip to content
Merged
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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,29 @@ jobs:
go-version: '1.23.x'
cache: true
- run: ./scripts/build-release.sh ci

web-templates:
name: Web template (${{ matrix.template }}, Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
template: [vanilla-js, react]
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23.x'
cache: true
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: templates/${{ matrix.template }}-foundations/package-lock.json
- name: Generate the bundled template
run: go run ./cmd/foundry new web-check --template ${{ matrix.template }} --destination "$RUNNER_TEMP/foundry-web" --no-hooks
- run: npm ci
working-directory: ${{ runner.temp }}/foundry-web
- run: npm run check
working-directory: ${{ runner.temp }}/foundry-web
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- Complete the project metadata package rename from `internal/projectconfig` to `internal/config`.

- Add embedded Vanilla JavaScript and React foundations with Vite, feature-first structure, reproducible npm installs, linting, formatting, behavior tests, and production builds.
- Expose both web templates in the CLI menu, aliases, generation metadata, and cross-version Node CI.

- Deliver all templates as embedded source files and restore `.tmpl` filenames during generation.
- Remove template ZIP packaging, downloads, external registries, and cache commands.
- Resolve template version checks and upgrade reports from the bundled registry only.
Expand Down
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Foundry is a single Go CLI for creating and extending projects from reusable fou

## Highlights

- Go and Flutter templates bundled directly as source files in one executable
- Go, Flutter, Vanilla JavaScript, and React templates bundled directly as source files in one executable
- offline project generation with no template downloads or packaging step
- `foundry template list` to inspect the bundled templates
- `foundry add service` and `foundry add migration` for Go projects
Expand All @@ -14,6 +14,8 @@ Foundry is a single Go CLI for creating and extending projects from reusable fou

- `go-foundations` (`go`, `grpc`, `backend`) — Go, gRPC, Protobuf, Clean Architecture, PostgreSQL, local JWT, optional OIDC, local RBAC, OpenTelemetry, Prometheus, Docker, migrations and tests.
- `flutter-foundations` (`flutter`, `mobile`, `app`) — Flutter, Riverpod, go_router, Dio, Melos, Mason-ready feature generation and reusable foundation packages.
- `vanilla-js-foundations` (`vanilla-js`, `vanilla`, `javascript`, `js`) — JavaScript ES modules, Vite, feature-first DOM components, ESLint, Prettier, Vitest, and Testing Library.
- `react-foundations` (`react`) — React with JavaScript, Vite, feature-first components, Hooks linting, ESLint, Prettier, Vitest, and Testing Library.

## Install

Expand Down Expand Up @@ -73,7 +75,27 @@ foundry new mediguide \
--org ug.go.health
```

Both templates are available offline. Optional generation hooks may invoke installed SDKs and their package managers; use `--no-hooks` to skip them.
Vanilla JavaScript:

```bash
foundry new website --template vanilla-js
cd website
npm ci
npm run dev
```

React (JavaScript):

```bash
foundry new dashboard --template react
cd dashboard
npm ci
npm run dev
```

The web templates use Node.js 22.18+ on the 22.x line, Node 24, or a newer supported even-numbered release. They include a feature-first `src/app`, `src/pages`, `src/features`, and `src/shared` structure, colocated tests, public environment configuration, reusable UI/styles, and an HTTP helper. Run `npm run check` for formatting, lint, tests, and a production build. Dependency installation is explicit; Foundry does not run npm automatically.

All templates are available offline. Optional generation hooks may invoke installed SDKs and their package managers; use `--no-hooks` to skip them.

## Extend an existing project

Expand Down Expand Up @@ -105,6 +127,8 @@ All templates are maintained under `templates/` and embedded directly into the F
| --- | --- | --- |
| Go Foundations | `templates/go-foundations/` | Embedded source files |
| Flutter Foundations | `templates/flutter_foundations/` | Embedded source files |
| Vanilla JavaScript Foundations | `templates/vanilla-js-foundations/` | Embedded source files |
| React Foundations | `templates/react-foundations/` | Embedded source files |

Go source files use `.go.tmpl` and its module manifest uses `go.mod.tmpl` to keep the template separate from Foundry's Go module. Generated projects receive ordinary `.go` files and `go.mod`.

Expand All @@ -116,7 +140,7 @@ To update a template, edit its source files and rebuild Foundry with `make build

```text
foundry
foundry new <name> --template <go|flutter>
foundry new <name> --template <go|flutter|vanilla-js|react>
foundry add service <name>
foundry add migration <name>
foundry add feature <name>
Expand Down Expand Up @@ -148,7 +172,9 @@ Foundry CLI
+-- built-in registry
+-- project generator
| +-- Go foundation
| `-- Flutter foundation
| +-- Flutter foundation
| +-- Vanilla JavaScript foundation
| `-- React foundation
|
+-- add generators
| +-- Go service
Expand All @@ -170,7 +196,7 @@ make build
make smoke
```

`go test ./...` checks bundled template generation, module substitution, CLI behavior, and the Go/Flutter add generators.
`go test ./...` checks generation of all four bundled templates, project setting substitution, CLI behavior, and the Go/Flutter add generators. Web-template CI generates fresh projects and runs `npm ci` and `npm run check` on Node 22 and 24.

### Releases

Expand Down
8 changes: 8 additions & 0 deletions assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ import "embed"
//go:embed all:templates/flutter_foundations/pubspec.lock
//go:embed all:templates/flutter_foundations/pubspec.yaml
//go:embed all:templates/flutter_foundations/scripts
//go:embed templates/vanilla-js-foundations/src templates/vanilla-js-foundations/index.html templates/vanilla-js-foundations/package*.json
//go:embed templates/vanilla-js-foundations/*.config.js templates/vanilla-js-foundations/README.md all:templates/vanilla-js-foundations/public
//go:embed templates/vanilla-js-foundations/.editorconfig templates/vanilla-js-foundations/.env.example templates/vanilla-js-foundations/.gitignore
//go:embed templates/vanilla-js-foundations/.nvmrc templates/vanilla-js-foundations/.prettierignore templates/vanilla-js-foundations/.prettierrc.json
//go:embed templates/react-foundations/src templates/react-foundations/index.html templates/react-foundations/package*.json
//go:embed templates/react-foundations/*.config.js templates/react-foundations/README.md all:templates/react-foundations/public
//go:embed templates/react-foundations/.editorconfig templates/react-foundations/.env.example templates/react-foundations/.gitignore
//go:embed templates/react-foundations/.nvmrc templates/react-foundations/.prettierignore templates/react-foundations/.prettierrc.json
var Templates embed.FS
23 changes: 18 additions & 5 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,22 @@ func (a App) interactive() int {
fmt.Fprintln(os.Stderr, "project name required")
return 2
}
fmt.Println("Template:\n 1) Go Foundations\n 2) Flutter Foundations")
fmt.Println("Template:\n 1) Go Foundations\n 2) Flutter Foundations\n 3) Vanilla JavaScript Foundations\n 4) React Foundations")
fmt.Print("Select [1]: ")
choice, _ := r.ReadString('\n')
choice = strings.TrimSpace(choice)
tmpl := "go"
if choice == "2" {
switch choice {
case "", "1":
case "2":
tmpl = "flutter"
case "3":
tmpl = "vanilla-js"
case "4":
tmpl = "react"
default:
fmt.Fprintln(os.Stderr, "select a template from 1 to 4")
return 2
}
return a.newCmd([]string{name, "--template", tmpl})
}
Expand All @@ -98,7 +107,7 @@ func (a App) newCmd(args []string) int {
}
f := flag.NewFlagSet("new", flag.ContinueOnError)
f.SetOutput(os.Stderr)
tmpl := f.String("template", "", "template: go or flutter")
tmpl := f.String("template", "", "template: go, flutter, vanilla-js, or react")
version := f.String("version", "", "template version; defaults to bundled version")
module := f.String("module", "", "Go module path")
org := f.String("org", "", "Flutter organization, e.g. ug.go.health")
Expand All @@ -113,11 +122,11 @@ func (a App) newCmd(args []string) int {
name = f.Args()[0]
}
if name == "" {
fmt.Fprintln(os.Stderr, "usage: foundry new <name> --template <go|flutter>")
fmt.Fprintln(os.Stderr, "usage: foundry new <name> --template <go|flutter|vanilla-js|react>")
return 2
}
if *tmpl == "" {
fmt.Fprintln(os.Stderr, "--template is required (go or flutter)")
fmt.Fprintln(os.Stderr, "--template is required (go, flutter, vanilla-js, or react)")
return 2
}

Expand All @@ -137,6 +146,9 @@ func (a App) newCmd(args []string) int {
return 1
}
fmt.Println("Template source: bundled files")
if selected.Kind == "javascript" || selected.Kind == "react" {
fmt.Println("Next: run npm ci, then npm run dev in the generated project.")
}
return 0
}

Expand Down Expand Up @@ -269,6 +281,7 @@ Usage:
foundry
foundry new <name> --template go [--module path] [--version version]
foundry new <name> --template flutter [--org com.example] [--version version]
foundry new <name> --template <vanilla-js|react> [--version version]
foundry add service <name> [--project path]
foundry add repository <name> [--project path]
foundry add usecase <domain> <name> [--project path]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package projectconfig
package config

import (
"bufio"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package projectconfig
package config

import (
"os"
Expand Down
2 changes: 1 addition & 1 deletion internal/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Check struct {
func Run() []Check {
checks := []Check{{Name: "OS", Detail: runtime.GOOS + "/" + runtime.GOARCH, OK: true}}
for _, c := range []struct{ Name, Bin string }{
{"Git", "git"}, {"Go", "go"}, {"Docker", "docker"}, {"Buf", "buf"},
{"Git", "git"}, {"Node.js", "node"}, {"npm", "npm"}, {"Go", "go"}, {"Docker", "docker"}, {"Buf", "buf"},
{"grpcurl", "grpcurl"}, {"Flutter", "flutter"}, {"Dart", "dart"}, {"Melos", "melos"}, {"Mason", "mason"},
} {
p, err := exec.LookPath(c.Bin)
Expand Down
6 changes: 3 additions & 3 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"

"github.com/jabahum/foundry/internal/projectconfig"
"github.com/jabahum/foundry/internal/config"
"github.com/jabahum/foundry/internal/registry"
)

Expand Down Expand Up @@ -64,7 +64,7 @@ func (g Generator) Generate(t registry.Template, o Options) error {
fmt.Fprintf(os.Stderr, "warning: post-generation hook: %v\n", err)
}
}
if err := projectconfig.Write(dest, projectconfig.Config{ProjectName: sanitize(o.Name), Template: t.ID, TemplateVersion: t.Version, Kind: t.Kind, Module: o.Module, Org: o.Org, Package: o.Package}); err != nil {
if err := config.Write(dest, config.Config{ProjectName: sanitize(o.Name), Template: t.ID, TemplateVersion: t.Version, Kind: t.Kind, Module: o.Module, Org: o.Org, Package: o.Package}); err != nil {
return fmt.Errorf("write .foundry.yaml: %w", err)
}
fmt.Printf("\nCreated %s using %s v%s\n", dest, t.Name, t.Version)
Expand Down Expand Up @@ -107,7 +107,7 @@ func copyTree(src fs.FS, root, dst string) error {
}

func replaceText(root string, vars map[string]string) error {
exts := map[string]bool{".go": true, ".mod": true, ".sum": true, ".proto": true, ".yaml": true, ".yml": true, ".json": true, ".md": true, ".sql": true, ".env": true, ".example": true, ".dart": true, ".xml": true, ".gradle": true, ".properties": true, ".toml": true, ".makefile": true, ".tmpl": true}
exts := map[string]bool{".html": true, ".js": true, ".jsx": true, ".mjs": true, ".css": true, ".go": true, ".mod": true, ".sum": true, ".proto": true, ".yaml": true, ".yml": true, ".json": true, ".md": true, ".sql": true, ".env": true, ".example": true, ".dart": true, ".xml": true, ".gradle": true, ".properties": true, ".toml": true, ".makefile": true, ".tmpl": true}
ordered := [][2]string{
{"example.com/go-foundations", vars["example.com/go-foundations"]},
{"go-foundations", vars["go-foundations"]},
Expand Down
59 changes: 56 additions & 3 deletions internal/generator/generator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package generator

import (
"encoding/json"
"io/fs"
"os"
"path/filepath"
Expand All @@ -10,11 +11,12 @@ import (
"testing/fstest"

"github.com/jabahum/foundry"
"github.com/jabahum/foundry/internal/config"
"github.com/jabahum/foundry/internal/registry"
)

func TestBundledTemplatesGenerate(t *testing.T) {
for _, name := range []string{"go", "flutter"} {
for _, name := range []string{"go", "flutter", "vanilla-js", "react"} {
t.Run(name, func(t *testing.T) {
tmpl, err := registry.Find(name)
if err != nil {
Expand All @@ -32,7 +34,7 @@ func TestBundledTemplatesGenerate(t *testing.T) {
if strings.HasSuffix(path, ".tmpl") {
t.Errorf("unrestored template filename: %s", path)
}
if d.IsDir() && (d.Name() == ".dart_tool" || d.Name() == ".fvm" || d.Name() == ".mason") {
if d.IsDir() && (d.Name() == ".dart_tool" || d.Name() == ".fvm" || d.Name() == ".mason" || d.Name() == "node_modules" || d.Name() == "dist") {
t.Errorf("local tool cache delivered: %s", path)
}
return nil
Expand All @@ -47,7 +49,7 @@ func TestBundledTemplatesGenerate(t *testing.T) {
t.Fatal(err)
}
}
if runtime.GOOS != "windows" {
if runtime.GOOS != "windows" && (name == "go" || name == "flutter") {
script := "scripts/setup.sh"
if name == "go" {
script = "scripts/generate-dev-keys.sh"
Expand All @@ -63,6 +65,8 @@ func TestBundledTemplatesGenerate(t *testing.T) {
file, expected := "pubspec.yaml", "name: flutter_foundation_workspace"
if name == "go" {
file, expected = "go.mod", "module demo\n"
} else if name == "vanilla-js" || name == "react" {
file, expected = "package.json", `"name": "demo"`
}
data, err := os.ReadFile(filepath.Join(dest, file))
if err != nil {
Expand Down Expand Up @@ -118,3 +122,52 @@ func TestSanitize(t *testing.T) {
}
}
}

func TestWebTemplatesSubstituteProjectSettings(t *testing.T) {
for _, name := range []string{"vanilla-js", "react"} {
t.Run(name, func(t *testing.T) {
tmpl, err := registry.Find(name)
if err != nil {
t.Fatal(err)
}
dest := t.TempDir()
if err := (Generator{FS: foundry.Templates}).Generate(tmpl, Options{Name: "Customer Portal", Destination: dest, NoHooks: true}); err != nil {
t.Fatal(err)
}
for _, file := range []string{"package.json", "package-lock.json"} {
data, err := os.ReadFile(filepath.Join(dest, file))
if err != nil {
t.Fatal(err)
}
var manifest struct {
Name string `json:"name"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
t.Fatal(err)
}
if manifest.Name != "customer_portal" {
t.Fatalf("%s: unexpected package name %q", file, manifest.Name)
}
if strings.Contains(string(data), "__PROJECT_NAME__") {
t.Errorf("unreplaced name in %s", file)
}
}
for _, file := range []string{"index.html", "src/shared/config/env.js", "README.md"} {
data, err := os.ReadFile(filepath.Join(dest, file))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "customer_portal") || strings.Contains(string(data), "__PROJECT_NAME__") {
t.Errorf("project name missing from %s", file)
}
}
config, err := config.Read(dest)
if err != nil {
t.Fatal(err)
}
if config.Kind != tmpl.Kind || config.Template != tmpl.ID {
t.Fatalf("wrong project metadata: %#v", config)
}
})
}
}
10 changes: 10 additions & 0 deletions internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ var templates = []Template{
Description: "Flutter + Riverpod + go_router + Dio + Melos + Mason-ready reusable foundation packages",
Aliases: []string{"flutter", "mobile", "app"}, EmbeddedDir: "templates/flutter_foundations", Kind: "flutter",
},
{
ID: "vanilla-js-foundations", Name: "Vanilla JavaScript Foundations", Version: "1.0.0",
Description: "JavaScript + Vite + feature-first modules + ESLint + Prettier + Vitest + accessible DOM components",
Aliases: []string{"vanilla", "vanilla-js", "javascript", "js"}, EmbeddedDir: "templates/vanilla-js-foundations", Kind: "javascript",
},
{
ID: "react-foundations", Name: "React Foundations", Version: "1.0.0",
Description: "React + JavaScript + Vite + feature-first components + Hooks linting + Testing Library + Vitest",
Aliases: []string{"react"}, EmbeddedDir: "templates/react-foundations", Kind: "react",
},
}

func All() []Template {
Expand Down
2 changes: 1 addition & 1 deletion internal/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package registry
import "testing"

func TestFindAliases(t *testing.T) {
for _, name := range []string{"go-foundations", "go", "grpc", "flutter", "mobile"} {
for _, name := range []string{"go-foundations", "go", "grpc", "flutter", "mobile", "vanilla-js-foundations", "vanilla", "vanilla-js", "javascript", "js", "react-foundations", "react"} {
if _, err := Find(name); err != nil {
t.Fatalf("Find(%q): %v", name, err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/upgrade/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package upgrade
import (
"fmt"

"github.com/jabahum/foundry/internal/projectconfig"
"github.com/jabahum/foundry/internal/config"
"github.com/jabahum/foundry/internal/registry"
)

Expand All @@ -13,7 +13,7 @@ type Report struct {
}

func Check(root string) (Report, error) {
c, err := projectconfig.Read(root)
c, err := config.Read(root)
if err != nil {
return Report{}, err
}
Expand Down
Loading
Loading