-
Notifications
You must be signed in to change notification settings - Fork 0
add server framework detection and overrides for tanstack #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
HomelessDinosaur
wants to merge
1
commit into
main
Choose a base branch
from
NIT-1230
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| ) | ||
|
|
||
| type packageJSON struct { | ||
| Scripts map[string]string `json:"scripts"` | ||
| Dependencies map[string]string `json:"dependencies"` | ||
| DevDependencies map[string]string `json:"devDependencies"` | ||
| } | ||
|
|
||
| func (p *packageJSON) hasAnyDependency(names ...string) bool { | ||
| for _, name := range names { | ||
| if _, ok := p.Dependencies[name]; ok { | ||
| return true | ||
| } | ||
| if _, ok := p.DevDependencies[name]; ok { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // serverFramework describes how a framework should be deployed once detected. | ||
| type serverFramework struct { | ||
| Name string | ||
| StartCmd string | ||
| } | ||
|
|
||
| // frameworkContext is the read access detectors get to the project source. | ||
| // Widen it (e.g. with the source root + a file reader for Astro's | ||
| // astro.config.mjs) as frameworks need more than dependency names; detector | ||
| // signatures stay the same. | ||
| type frameworkContext struct { | ||
| pkg *packageJSON | ||
| } | ||
|
|
||
| type serverFrameworkDetector func(*frameworkContext) (serverFramework, bool) | ||
|
|
||
| // serverFrameworkDetectors lists the frameworks we steer onto railpack's | ||
| // server path. Add a framework by writing a detector and appending it here. | ||
| var serverFrameworkDetectors = []serverFrameworkDetector{ | ||
| detectTanstackStart, | ||
| } | ||
|
|
||
| func detectTanstackStart(c *frameworkContext) (serverFramework, bool) { | ||
| // Scaffolded with the Nitro adapter, TanStack Start has no `start` script, | ||
| // so railpack serves the build output as a static SPA. Nitro emits a server | ||
| // at .output/server/index.mjs (same as Nuxt). @tanstack/start is the old name. | ||
| if c.pkg.hasAnyDependency("@tanstack/react-start", "@tanstack/solid-start", "@tanstack/start") { | ||
| return serverFramework{Name: "TanStack Start", StartCmd: "node .output/server/index.mjs"}, true | ||
| } | ||
| return serverFramework{}, false | ||
| } | ||
|
|
||
| // detectServerFramework finds full-stack frameworks that railpack misclassifies | ||
| // as static SPAs. railpack reads Vite + a build script + no `start` script as | ||
| // an SPA and serves it with Caddy (NIT-1230). We only intervene when there is | ||
| // no `start` script — with one, railpack already deploys a server. | ||
| func detectServerFramework(sourceDir string) (serverFramework, bool) { | ||
| pkg, err := readPackageJSON(sourceDir) | ||
| if err != nil { | ||
| return serverFramework{}, false | ||
| } | ||
| if strings.TrimSpace(pkg.Scripts["start"]) != "" { | ||
| return serverFramework{}, false | ||
| } | ||
|
|
||
| ctx := &frameworkContext{pkg: pkg} | ||
| for _, detect := range serverFrameworkDetectors { | ||
| if fw, ok := detect(ctx); ok { | ||
| return fw, true | ||
| } | ||
| } | ||
| return serverFramework{}, false | ||
| } | ||
|
|
||
| func readPackageJSON(sourceDir string) (*packageJSON, error) { | ||
| data, err := os.ReadFile(filepath.Join(sourceDir, "package.json")) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var pkg packageJSON | ||
| if err := json.Unmarshal(data, &pkg); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &pkg, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| func writePackageJSON(t *testing.T, contents string) string { | ||
| t.Helper() | ||
| dir := t.TempDir() | ||
| if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(contents), 0644); err != nil { | ||
| t.Fatalf("writing package.json: %v", err) | ||
| } | ||
| return dir | ||
| } | ||
|
|
||
| func TestDetectServerFramework_TanstackStart(t *testing.T) { | ||
| cases := []struct { | ||
| name string | ||
| pkg string | ||
| }{ | ||
| { | ||
| name: "react-start in dependencies", | ||
| pkg: `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"@tanstack/react-start": "^1.0.0", "vite": "^6.0.0"} | ||
| }`, | ||
| }, | ||
| { | ||
| name: "solid-start in devDependencies", | ||
| pkg: `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "devDependencies": {"@tanstack/solid-start": "^1.0.0"} | ||
| }`, | ||
| }, | ||
| { | ||
| name: "legacy @tanstack/start", | ||
| pkg: `{"scripts": {"build": "vite build"}, "dependencies": {"@tanstack/start": "^1.0.0"}}`, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| dir := writePackageJSON(t, tc.pkg) | ||
| fw, ok := detectServerFramework(dir) | ||
| if !ok { | ||
| t.Fatalf("expected TanStack Start to be detected") | ||
| } | ||
| if fw.Name != "TanStack Start" { | ||
| t.Errorf("name = %q, want %q", fw.Name, "TanStack Start") | ||
| } | ||
| if fw.StartCmd != "node .output/server/index.mjs" { | ||
| t.Errorf("startCmd = %q, want %q", fw.StartCmd, "node .output/server/index.mjs") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestDetectServerFramework_StartScriptWins(t *testing.T) { | ||
| // An explicit start script means railpack already deploys it as a server, | ||
| // so we must not override. | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build", "start": "node ./server.js"}, | ||
| "dependencies": {"@tanstack/react-start": "^1.0.0"} | ||
| }`) | ||
|
|
||
| if _, ok := detectServerFramework(dir); ok { | ||
| t.Errorf("expected no override when a start script is present") | ||
| } | ||
| } | ||
|
|
||
| func TestDetectServerFramework_PlainVite(t *testing.T) { | ||
| // A plain Vite SPA should be left alone for railpack to serve statically. | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"vite": "^6.0.0", "react": "^19.0.0"} | ||
| }`) | ||
|
|
||
| if _, ok := detectServerFramework(dir); ok { | ||
| t.Errorf("expected no override for a plain Vite SPA") | ||
| } | ||
| } | ||
|
|
||
| func TestDetectServerFramework_NoPackageJSON(t *testing.T) { | ||
| if _, ok := detectServerFramework(t.TempDir()); ok { | ||
| t.Errorf("expected no detection when package.json is absent") | ||
| } | ||
| } | ||
|
|
||
| func TestHasConfigVar(t *testing.T) { | ||
| envs := []string{"NODE_ENV=production", "RAILPACK_NO_SPA=1"} | ||
| if !hasConfigVar(envs, "RAILPACK_NO_SPA") { | ||
| t.Errorf("expected RAILPACK_NO_SPA to be detected") | ||
| } | ||
| if hasConfigVar(envs, "RAILPACK_SPA_OUTPUT_DIR") { | ||
| t.Errorf("did not expect RAILPACK_SPA_OUTPUT_DIR to be detected") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "slices" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestApplyServerFrameworkOverrides_TanstackStart(t *testing.T) { | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"@tanstack/react-start": "^1.0.0"} | ||
| }`) | ||
| opts := PlannerOptions{SourceDir: dir} | ||
|
|
||
| applyServerFrameworkOverrides(&opts) | ||
|
|
||
| if opts.StartCmd != "node .output/server/index.mjs" { | ||
| t.Errorf("startCmd = %q, want injected server command", opts.StartCmd) | ||
| } | ||
| // railpack turns plan-time envs into build secrets, so we must not inject | ||
| // RAILPACK_NO_SPA — the start command alone forces the server path. | ||
| if len(opts.Envs) != 0 { | ||
| t.Errorf("envs = %v, want none injected", opts.Envs) | ||
| } | ||
| } | ||
|
|
||
| func TestApplyServerFrameworkOverrides_ExplicitStartCmdWins(t *testing.T) { | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"@tanstack/react-start": "^1.0.0"} | ||
| }`) | ||
| opts := PlannerOptions{SourceDir: dir, StartCmd: "node custom.js"} | ||
|
|
||
| applyServerFrameworkOverrides(&opts) | ||
|
|
||
| if opts.StartCmd != "node custom.js" { | ||
| t.Errorf("startCmd = %q, want it left untouched", opts.StartCmd) | ||
| } | ||
| if slices.Contains(opts.Envs, "RAILPACK_NO_SPA=1") { | ||
| t.Errorf("did not expect RAILPACK_NO_SPA to be injected over an explicit start command") | ||
| } | ||
| } | ||
|
|
||
| func TestApplyServerFrameworkOverrides_SPAOptInWins(t *testing.T) { | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"@tanstack/react-start": "^1.0.0"} | ||
| }`) | ||
| opts := PlannerOptions{SourceDir: dir, Envs: []string{"RAILPACK_SPA_OUTPUT_DIR=dist"}} | ||
|
|
||
| applyServerFrameworkOverrides(&opts) | ||
|
|
||
| if opts.StartCmd != "" { | ||
| t.Errorf("startCmd = %q, want empty when user opted into an SPA build", opts.StartCmd) | ||
| } | ||
| } | ||
|
|
||
| func TestApplyServerFrameworkOverrides_PlainViteUntouched(t *testing.T) { | ||
| dir := writePackageJSON(t, `{ | ||
| "scripts": {"build": "vite build"}, | ||
| "dependencies": {"vite": "^6.0.0"} | ||
| }`) | ||
| opts := PlannerOptions{SourceDir: dir} | ||
|
|
||
| applyServerFrameworkOverrides(&opts) | ||
|
|
||
| if opts.StartCmd != "" || len(opts.Envs) != 0 { | ||
| t.Errorf("plain Vite SPA should be left untouched, got startCmd=%q envs=%v", opts.StartCmd, opts.Envs) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use the detected runtime? If the railpack output uses a bun base image for the runtime as an example this will fail as node will not be available