package node import ( "fmt" "testing" "github.com/hanzoai/pack/core/app" "github.com/hanzoai/pack/core/generate" "github.com/hanzoai/pack/core/plan" testingUtils "github.com/hanzoai/pack/core/testing" "github.com/stretchr/testify/require" ) func TestNode(t *testing.T) { tests := []struct { name string path string detected bool packageManager PackageManager nodeVersion string pnpmVersion string envVars map[string]string }{ { name: "npm", path: "../../../examples/node-npm", detected: true, packageManager: PackageManagerNpm, nodeVersion: "23.5.0", }, { name: "bun", path: "../../../examples/node-bun", detected: true, packageManager: PackageManagerBun, }, { name: "pnpm", path: "../../../examples/node-corepack", detected: true, packageManager: PackageManagerPnpm, nodeVersion: "20", pnpmVersion: "10.4.1", }, { name: "pnpm", path: "../../../examples/node-pnpm-workspaces", detected: true, packageManager: PackageManagerPnpm, nodeVersion: "22.2.0", }, { name: "pnpm from mise.toml", path: "../../../examples/node-latest-pnpm-mise-native-deps", detected: true, packageManager: PackageManagerPnpm, nodeVersion: "latest", pnpmVersion: "latest", }, { name: "pnpm", path: "../../../examples/node-astro", detected: true, packageManager: PackageManagerNpm, }, { name: "railpack node version overrides engines", path: "../../../examples/node-version-precedence", detected: true, packageManager: PackageManagerNpm, nodeVersion: "22", envVars: map[string]string{"RAILPACK_NODE_VERSION": "22"}, }, { name: "golang", path: "../../../examples/go-mod", detected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := testingUtils.CreateGenerateContext(t, tt.path) if tt.envVars != nil { envVars := tt.envVars ctx.Env = app.NewEnvironment(&envVars) } provider := NodeProvider{} detected, err := provider.Detect(ctx) require.NoError(t, err) require.Equal(t, tt.detected, detected) if detected { err = provider.Initialize(ctx) require.NoError(t, err) require.Equal(t, tt.packageManager, provider.packageManager) err = provider.Plan(ctx) require.NoError(t, err) if tt.nodeVersion != "" { nodeVersion := ctx.Resolver.Get("node") if tt.nodeVersion == "latest" { require.NotEmpty(t, nodeVersion.Version) } else { require.Equal(t, tt.nodeVersion, nodeVersion.Version) } } if tt.pnpmVersion != "" { pnpmVersion := ctx.Resolver.Get("pnpm") require.NotNil(t, pnpmVersion) if tt.pnpmVersion == "latest" { require.NotEmpty(t, pnpmVersion.Version) } else { require.Equal(t, tt.pnpmVersion, pnpmVersion.Version) } } } }) } } func TestNodeCorepack(t *testing.T) { tests := []struct { name string path string wantCorepack bool }{ { name: "corepack project", path: "../../../examples/node-corepack", wantCorepack: true, }, { name: "bun project", path: "../../../examples/node-bun", wantCorepack: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := testingUtils.CreateGenerateContext(t, tt.path) provider := NodeProvider{} err := provider.Initialize(ctx) require.NoError(t, err) usesCorepack := provider.usesCorepack() require.Equal(t, tt.wantCorepack, usesCorepack) }) } } func TestGetNextApps(t *testing.T) { tests := []struct { name string path string want []string }{ { name: "npm project", path: "../../../examples/node-npm", want: []string{}, }, { name: "bun project", path: "../../../examples/node-next", want: []string{""}, }, { name: "turbo with 2 next apps", path: "../../../examples/node-turborepo", want: []string{"apps/web"}, }, { name: "nx next workspace", path: "../../../examples/node-nx-next", want: []string{"apps/web"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := testingUtils.CreateGenerateContext(t, tt.path) provider := NodeProvider{} err := provider.Initialize(ctx) require.NoError(t, err) nextPackages, err := provider.getPackagesWithFramework(ctx, func(pkg *WorkspacePackage, ctx *generate.GenerateContext) bool { if pkg.PackageJson.BuildScriptContains("next build") { return true } return provider.isNextAppPackage(pkg, ctx) }) require.NoError(t, err) nextApps := make([]string, len(nextPackages)) for i, pkg := range nextPackages { nextApps[i] = pkg.Path } require.Equal(t, tt.want, nextApps) }) } } func TestPackageJsonRequiresBun(t *testing.T) { // Special cases t.Run("nil package.json", func(t *testing.T) { got := packageJsonRequiresBun(nil) require.False(t, got) }) t.Run("no scripts", func(t *testing.T) { got := packageJsonRequiresBun(&PackageJson{}) require.False(t, got) }) // Scripts that should trigger bun detection bunScripts := []string{ "bun run server.js", "bunx nodemon index.js", "bun test", "npm run clean && bun build.js", "echo 'Running tests' | bun test", "npm run build; bun run server.js", "cd src && bun install", "bun --version", "bunx prisma migrate", } t.Run("scripts requiring bun", func(t *testing.T) { packageJson := &PackageJson{ Scripts: make(map[string]string), } for i, script := range bunScripts { packageJson.Scripts[fmt.Sprintf("script%d", i)] = script } got := packageJsonRequiresBun(packageJson) require.True(t, got) }) // Scripts that should NOT trigger bun detection nonBunScripts := []string{ "esbuild dev.ts ./src --bundle --outdir=dist --packages=external --platform=node --sourcemap --watch", "webpack --config webpack.bundle.config.js", "node src/bundle-manager.js", "jest --bundle-reporter", "eslint src/bundles/", "sh deploy-bundle.sh", "npm run bundle:production", "yarn bundle", "pnpm run unbundle", } t.Run("scripts not requiring bun", func(t *testing.T) { packageJson := &PackageJson{ Scripts: make(map[string]string), } for i, script := range nonBunScripts { packageJson.Scripts[fmt.Sprintf("script%d", i)] = script } got := packageJsonRequiresBun(packageJson) require.False(t, got) }) } func TestUsesPnpmBinSubdir(t *testing.T) { tests := []struct { name string version string want bool }{ { name: "latest uses bin subdir", version: "latest", want: true, }, { name: "major 11 uses bin subdir", version: "11", want: true, }, { name: "pnpm 11 uses bin subdir", version: "11.0.0", want: true, }, { name: "pnpm 10 does not use bin subdir", version: "10.9.0", want: false, }, { name: "empty version does not use bin subdir", version: "", want: false, }, { name: "invalid version does not use bin subdir", version: "workspace:^", want: false, }, { name: "resolved mise version", version: "11.5.1", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, usesPnpmBinSubdir(tt.version)) }) } } func TestPlaywrightInstallationIsOptIn(t *testing.T) { t.Run("does not install by default", func(t *testing.T) { ctx := testingUtils.CreateGenerateContext(t, "../../../examples/node-playwright") provider := NodeProvider{} require.NoError(t, provider.Initialize(ctx)) require.NoError(t, provider.Plan(ctx)) require.False(t, nodeStepHasExecCommand( ctx, "install", "pnpm exec playwright install --only-shell", )) require.NotContains(t, ctx.Deploy.AptPackages, "libnss3") }) t.Run("installs when enabled", func(t *testing.T) { ctx := testingUtils.CreateGenerateContext(t, "../../../examples/node-playwright") ctx.Env.Variables["RAILPACK_NODE_PLAYWRIGHT_INSTALL"] = "1" provider := NodeProvider{} require.NoError(t, provider.Initialize(ctx)) require.NoError(t, provider.Plan(ctx)) require.True(t, nodeStepHasExecCommand( ctx, "install", "pnpm exec playwright install --only-shell", )) require.Contains(t, ctx.Deploy.AptPackages, "libnss3") }) } func nodeStepHasExecCommand( ctx *generate.GenerateContext, stepName string, command string, ) bool { step := ctx.GetStepByName(stepName) if step == nil { return false } commandStep, ok := (*step).(*generate.CommandStepBuilder) if !ok { return false } for _, candidate := range commandStep.Commands { execCommand, ok := candidate.(plan.ExecCommand) if ok && execCommand.Cmd == command { return true } } return false }