mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat(node): support package.json5 manifests (#566)
* feat(node): add support for package.json5 - introduce findPackageManifest to prioritize package.json over package.json5 - update dependency detection and installation logic to include package.json5 - ensure local file detection and workspace parsing support the new manifest format Generated-by: aiautocommit * feat(example): add node-pnpm-json5 project * Add a new example project to demonstrate native pnpm support for package.json5 manifests. * Include a test configuration to verify environment detection and CLI version reporting. Generated-by: aiautocommit * chore: bump node and pnpm versions in json5 example Generated-by: aiautocommit * test: add build plan snapshot for pnpm-json5 project Generated-by: aiautocommit * test: update snapshots * chore(examples): update engine versions and test expectations Generated-by: aiautocommit * chore(deps): update pnpm-lock.yaml Generated-by: aiautocommit * docs: update mise configuration documentation and add package resolution notes * Update the `install_before` setting name to `minimum_release_age` to match current configuration. * Add clarification on how Railpack handles semver constraints from manifest files by simplifying ranges to major versions. Generated-by: aiautocommit * feat(example): log pnpm version in pnpm workspaces example - Add checks for pnpm version via npm_config_user_agent and the pnpm CLI. - Update test expectations to verify pnpm version reporting. Generated-by: aiautocommit * feat(node): support more pnpm lockfile version mapping - Add support for pnpm lockfile versions 6.1 and 9.0 to ensure correct tool version detection. - Refactor the lockfile version check into a switch statement to improve readability and maintainability. - Add logging for cases where the pnpm lockfile version cannot be identified. Generated-by: aiautocommit * test(node): add tests for pnpm version resolution logic - Verify that pnpm versions are correctly derived from pnpm-lock.yaml versioning. - Ensure proper precedence hierarchy between lockfile, engines, and packageManager fields. Generated-by: aiautocommit * docs: move mise installation instruction to workflow section Generated-by: aiautocommit * fix(node): ensure engines.pnpm overrides lockfile version inference - Reprioritize pnpm version resolution to prefer `engines` over lockfile-based inference. - Lockfile version parsing can be ambiguous across major versions, whereas `engines` provides an explicit developer intent. Generated-by: aiautocommit
This commit is contained in:
@@ -36,12 +36,9 @@ with support for Node, Python, Go, PHP, and more.
|
||||
or update lockfiles. Manual edits can result in invalid checksums and broken
|
||||
builds.
|
||||
|
||||
# Mise Setup
|
||||
|
||||
If mise is not available in your environment, install it using the instructions at https://mise.jdx.dev/installing-mise.html.
|
||||
|
||||
# Workflow
|
||||
|
||||
- If mise is not available in your environment, install it using the instructions at https://mise.jdx.dev/installing-mise.html.
|
||||
- Take a careful look at @mise.toml to understand what commands should be run at different points in the project lifecycle
|
||||
- Do not worry about docker cache, etc. Never run `docker system prune` or any other similar commands.
|
||||
- Do not run `go` directly. Instead, inspect @mise.toml and use `mise run <task>` to run various dev lifecycle commands. For instance, you should not run `go vet`, `go fmt`, `go test`, etc directly.
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"caches": {
|
||||
"apt": {
|
||||
"directory": "/var/cache/apt",
|
||||
"type": "locked"
|
||||
},
|
||||
"apt-lists": {
|
||||
"directory": "/var/lib/apt/lists",
|
||||
"type": "locked"
|
||||
},
|
||||
"node-modules": {
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"pnpm-install": {
|
||||
"directory": "/root/.local/share/pnpm/store/v3",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"step": "packages:apt:runtime"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/node_modules"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn"
|
||||
],
|
||||
"include": [
|
||||
"/root/.cache",
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "pnpm run start",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: node, pnpm"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.5.16"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"pnpm-install"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package.json5",
|
||||
"src": "package.json5"
|
||||
},
|
||||
{
|
||||
"dest": "pnpm-lock.yaml",
|
||||
"src": "pnpm-lock.yaml"
|
||||
},
|
||||
{
|
||||
"path": "/pnpm"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm add -g node-gyp"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm install --frozen-lockfile --prefer-offline"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"PNPM_HOME": "/pnpm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"node-modules"
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"apt",
|
||||
"apt-lists"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
|
||||
"customName": "install apt packages: libatomic1"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.5.16"
|
||||
}
|
||||
],
|
||||
"name": "packages:apt:runtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,8 +29,26 @@ const (
|
||||
var (
|
||||
// bunCommandRegex matches "bun" or "bunx" as a command (not part of another word)
|
||||
bunCommandRegex = regexp.MustCompile(`(^|\s|;|&|&&|\||\|\|)bunx?\s`)
|
||||
|
||||
// PackageManifestFiles is the precedence order Railpack searches for a Node
|
||||
// package manifest. package.json wins if both are present; package.json5 is
|
||||
// supported because pnpm reads it natively (https://pnpm.io/package_json).
|
||||
// Railpack's App.ReadJSON pipes through hujson, which accepts comments and
|
||||
// trailing commas — the common subset of JSON5 used in real manifests.
|
||||
PackageManifestFiles = []string{"package.json", "package.json5"}
|
||||
)
|
||||
|
||||
// findPackageManifest returns the first manifest file present in the app,
|
||||
// honoring PackageManifestFiles precedence. Returns "" if none are present.
|
||||
func findPackageManifest(app *app.App) string {
|
||||
for _, f := range PackageManifestFiles {
|
||||
if app.HasFile(f) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type NodeProvider struct {
|
||||
packageJson *PackageJson
|
||||
packageManager PackageManager
|
||||
@@ -60,7 +78,7 @@ func (p *NodeProvider) Initialize(ctx *generate.GenerateContext) error {
|
||||
}
|
||||
|
||||
func (p *NodeProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
|
||||
return ctx.App.HasFile("package.json"), nil
|
||||
return findPackageManifest(ctx.App) != "", nil
|
||||
}
|
||||
|
||||
func (p *NodeProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
@@ -267,7 +285,7 @@ func (p *NodeProvider) InstallNodeDeps(ctx *generate.GenerateContext, install *g
|
||||
ctx.Logger.LogInfo("Installing %s@%s with Corepack", pmName, pmVersion)
|
||||
|
||||
install.AddCommands([]plan.Command{
|
||||
plan.NewCopyCommand("package.json"),
|
||||
plan.NewCopyCommand(findPackageManifest(ctx.App)),
|
||||
// corepack will detect the package manager version from package.json, safe to assume the user is properly
|
||||
// specifying the version they want there, no need to check other version specifications.
|
||||
// corepack *used* to be bundled with node, but as of v25 it's not, so we install it explicitly
|
||||
@@ -457,13 +475,14 @@ func (p *NodeProvider) getPackageManager(app *app.App) PackageManager {
|
||||
|
||||
func (p *NodeProvider) GetPackageJson(app *app.App) (*PackageJson, error) {
|
||||
packageJson := NewPackageJson()
|
||||
if !app.HasFile("package.json") {
|
||||
manifest := findPackageManifest(app)
|
||||
if manifest == "" {
|
||||
return packageJson, nil
|
||||
}
|
||||
|
||||
err := app.ReadJSON("package.json", packageJson)
|
||||
err := app.ReadJSON(manifest, packageJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading package.json: %w", err)
|
||||
return nil, fmt.Errorf("error reading %s: %w", manifest, err)
|
||||
}
|
||||
|
||||
return packageJson, nil
|
||||
|
||||
@@ -51,8 +51,9 @@ func (p PackageManager) RunScriptCommand(cmd string) string {
|
||||
}
|
||||
|
||||
// Map the active package manager to the mise tool key whose app-local version
|
||||
// should override Railpack's inferred/default version. We can't use Name() here
|
||||
// because it answers "which command do we run"; for npm that would return
|
||||
// should override Railpack's inferred/default version.
|
||||
//
|
||||
// Can't use Name() because it answers "which command do we run"; for npm that would return
|
||||
// "npm", but npm is not installed as a separate mise-managed tool in this flow.
|
||||
// This lets us ask mise for the app's resolved tool version and keep Railpack's
|
||||
// package-manager selection consistent with the app's mise config.
|
||||
@@ -247,11 +248,12 @@ func (p PackageManager) pruneYarnBerry(ctx *generate.GenerateContext, prune *gen
|
||||
|
||||
func (p PackageManager) getPackageJsonFromContext(ctx *generate.GenerateContext) (*PackageJson, error) {
|
||||
packageJson := NewPackageJson()
|
||||
if !ctx.App.HasFile("package.json") {
|
||||
manifest := findPackageManifest(ctx.App)
|
||||
if manifest == "" {
|
||||
return packageJson, nil
|
||||
}
|
||||
|
||||
err := ctx.App.ReadJSON("package.json", packageJson)
|
||||
err := ctx.App.ReadJSON(manifest, packageJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -275,7 +277,7 @@ func (p PackageManager) GetInstallFolder(ctx *generate.GenerateContext) []string
|
||||
// SupportingInstallFiles returns a list of files that are needed to install dependencies
|
||||
func (p PackageManager) SupportingInstallFiles(ctx *generate.GenerateContext) []string {
|
||||
// Use brace expansion for single filesystem traversal instead of 16 separate globs
|
||||
pattern := "**/{package.json,package-lock.json,pnpm-workspace.yaml,yarn.lock,pnpm-lock.yaml,bun.lockb,bun.lock,bunfig.toml,.yarn,.pnp.*,.yarnrc.yml,.npmrc,.node-version,.nvmrc,patches,.pnpm-patches,prisma}"
|
||||
pattern := "**/{package.json,package.json5,package-lock.json,pnpm-workspace.yaml,yarn.lock,pnpm-lock.yaml,bun.lockb,bun.lock,bunfig.toml,.yarn,.pnp.*,.yarnrc.yml,.npmrc,.node-version,.nvmrc,patches,.pnpm-patches,prisma}"
|
||||
|
||||
var allFiles []string
|
||||
|
||||
@@ -312,22 +314,31 @@ func (p PackageManager) GetPackageManagerPackages(ctx *generate.GenerateContext,
|
||||
if p == PackageManagerPnpm {
|
||||
pnpm := packages.Default("pnpm", DEFAULT_PNPM_VERSION)
|
||||
|
||||
// Prefer explicit version from package.json engines over defaults/lockfile
|
||||
if packageJson != nil && packageJson.Engines != nil && packageJson.Engines["pnpm"] != "" {
|
||||
packages.Version(pnpm, packageJson.Engines["pnpm"], "package.json > engines > pnpm")
|
||||
}
|
||||
|
||||
lockfile, err := ctx.App.ReadFile("pnpm-lock.yaml")
|
||||
if err == nil {
|
||||
if strings.HasPrefix(lockfile, "lockfileVersion: 5.3") {
|
||||
switch {
|
||||
case strings.HasPrefix(lockfile, "lockfileVersion: 5.3"):
|
||||
packages.Version(pnpm, "6", "pnpm-lock.yaml")
|
||||
} else if strings.HasPrefix(lockfile, "lockfileVersion: 5.4") {
|
||||
case strings.HasPrefix(lockfile, "lockfileVersion: 5.4"):
|
||||
packages.Version(pnpm, "7", "pnpm-lock.yaml")
|
||||
} else if strings.HasPrefix(lockfile, "lockfileVersion: '6.0'") {
|
||||
case strings.HasPrefix(lockfile, "lockfileVersion: '6.0'") || strings.HasPrefix(lockfile, "lockfileVersion: 6.0"):
|
||||
packages.Version(pnpm, "8", "pnpm-lock.yaml")
|
||||
case strings.HasPrefix(lockfile, "lockfileVersion: '6.1'"):
|
||||
packages.Version(pnpm, "8", "pnpm-lock.yaml")
|
||||
case strings.HasPrefix(lockfile, "lockfileVersion: '9.0'") || strings.HasPrefix(lockfile, "lockfileVersion: 9.0"):
|
||||
// pnpm 9 introduced lockfileVersion 9.0.
|
||||
// pnpm 10 and 11 continue using the same 9.0 format (no bump).
|
||||
packages.Version(pnpm, "9", "pnpm-lock.yaml")
|
||||
default:
|
||||
log.Warnf("could not detect pnpm lockfile version")
|
||||
}
|
||||
}
|
||||
|
||||
// engines.pnpm overrides lockfile inference because lockfileVersion is ambiguous across majors
|
||||
if packageJson != nil && packageJson.Engines != nil && packageJson.Engines["pnpm"] != "" {
|
||||
packages.Version(pnpm, packageJson.Engines["pnpm"], "package.json > engines > pnpm")
|
||||
}
|
||||
|
||||
if pmName == "pnpm" && pmVersion != "" {
|
||||
packages.Version(pnpm, pmVersion, "package.json > packageManager")
|
||||
|
||||
@@ -383,7 +394,7 @@ func (p PackageManager) GetPackageManagerPackages(ctx *generate.GenerateContext,
|
||||
|
||||
// usesLocalFile returns true if the package.json has a local dependency (e.g. file:./path/to/package)
|
||||
func (p PackageManager) usesLocalFile(ctx *generate.GenerateContext) bool {
|
||||
files, err := ctx.App.FindFiles("**/package.json")
|
||||
files, err := ctx.App.FindFiles("**/{package.json,package.json5}")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/railwayapp/railpack/core/resolver"
|
||||
testingUtils "github.com/railwayapp/railpack/core/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPackageManagerPackages_PnpmLockfileVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lockfile string
|
||||
wantVersion string
|
||||
wantSource string
|
||||
}{
|
||||
{
|
||||
name: "lockfile 5.3",
|
||||
lockfile: "lockfileVersion: 5.3\n",
|
||||
wantVersion: "6",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 5.4",
|
||||
lockfile: "lockfileVersion: 5.4\n",
|
||||
wantVersion: "7",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 6.0 quoted",
|
||||
lockfile: "lockfileVersion: '6.0'\n",
|
||||
wantVersion: "8",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 6.0 unquoted",
|
||||
lockfile: "lockfileVersion: 6.0\n",
|
||||
wantVersion: "8",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 6.1",
|
||||
lockfile: "lockfileVersion: '6.1'\n",
|
||||
wantVersion: "8",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 9.0 quoted",
|
||||
lockfile: "lockfileVersion: '9.0'\n",
|
||||
wantVersion: "9",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "lockfile 9.0 unquoted",
|
||||
lockfile: "lockfileVersion: 9.0\n",
|
||||
wantVersion: "9",
|
||||
wantSource: "pnpm-lock.yaml",
|
||||
},
|
||||
{
|
||||
name: "unknown lockfile keeps default",
|
||||
lockfile: "lockfileVersion: '99.0'\n",
|
||||
wantVersion: DEFAULT_PNPM_VERSION,
|
||||
wantSource: resolver.DefaultSource,
|
||||
},
|
||||
{
|
||||
name: "no lockfile keeps default",
|
||||
lockfile: "",
|
||||
wantVersion: DEFAULT_PNPM_VERSION,
|
||||
wantSource: resolver.DefaultSource,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte("{}"), 0o644))
|
||||
if tt.lockfile != "" {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "pnpm-lock.yaml"), []byte(tt.lockfile), 0o644))
|
||||
}
|
||||
|
||||
ctx := testingUtils.CreateGenerateContext(t, tmpDir)
|
||||
miseStep := ctx.NewMiseStepBuilder("test")
|
||||
|
||||
PackageManagerPnpm.GetPackageManagerPackages(ctx, NewPackageJson(), miseStep)
|
||||
|
||||
pnpm := ctx.Resolver.Get("pnpm")
|
||||
require.NotNil(t, pnpm)
|
||||
require.Equal(t, tt.wantVersion, pnpm.Version)
|
||||
require.Equal(t, tt.wantSource, pnpm.Source)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPackageManagerPackages_PnpmVersionPrecedence(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte("{}"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "pnpm-lock.yaml"), []byte("lockfileVersion: '6.0'\n"), 0o644))
|
||||
|
||||
t.Run("engines override lockfile", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, tmpDir)
|
||||
miseStep := ctx.NewMiseStepBuilder("test")
|
||||
packageJson := NewPackageJson()
|
||||
packageJson.Engines["pnpm"] = "10"
|
||||
|
||||
PackageManagerPnpm.GetPackageManagerPackages(ctx, packageJson, miseStep)
|
||||
|
||||
pnpm := ctx.Resolver.Get("pnpm")
|
||||
require.NotNil(t, pnpm)
|
||||
require.Equal(t, "10", pnpm.Version)
|
||||
require.Equal(t, "package.json > engines > pnpm", pnpm.Source)
|
||||
})
|
||||
|
||||
t.Run("packageManager overrides lockfile", func(t *testing.T) {
|
||||
ctx := testingUtils.CreateGenerateContext(t, tmpDir)
|
||||
miseStep := ctx.NewMiseStepBuilder("test")
|
||||
pm := "[email protected]"
|
||||
packageJson := &PackageJson{PackageManager: &pm}
|
||||
|
||||
PackageManagerPnpm.GetPackageManagerPackages(ctx, packageJson, miseStep)
|
||||
|
||||
pnpm := ctx.Resolver.Get("pnpm")
|
||||
require.NotNil(t, pnpm)
|
||||
require.Equal(t, "10.4.1", pnpm.Version)
|
||||
require.Equal(t, "package.json > packageManager", pnpm.Source)
|
||||
require.True(t, pnpm.SkipMiseInstall)
|
||||
})
|
||||
}
|
||||
@@ -21,11 +21,15 @@ type PnpmWorkspace struct {
|
||||
Packages []string `yaml:"packages"`
|
||||
}
|
||||
|
||||
// NewWorkspace creates a new workspace from a package.json file
|
||||
// NewWorkspace creates a new workspace from a package.json (or package.json5) file
|
||||
func NewWorkspace(app *app.App) (*Workspace, error) {
|
||||
packageJson, err := readPackageJson(app, "package.json")
|
||||
manifest := findPackageManifest(app)
|
||||
if manifest == "" {
|
||||
manifest = "package.json"
|
||||
}
|
||||
packageJson, err := readPackageJson(app, manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading root package.json: %w", err)
|
||||
return nil, fmt.Errorf("error reading root %s: %w", manifest, err)
|
||||
}
|
||||
|
||||
workspace := &Workspace{
|
||||
|
||||
@@ -14,7 +14,8 @@ Railpack and alternative installation methods are possible (for example PHP will
|
||||
use Mise to resolve a valid version and then start from a PHP base image).
|
||||
Railpack enables Mise paranoid mode for stricter security validation.
|
||||
|
||||
For more information on how Railpack utilizes Mise and our philosophy on tool defaults, see the [Mise Configuration](/config/mise) guide.
|
||||
For more information on how Railpack utilizes Mise and our philosophy on tool
|
||||
defaults, see the [Mise Configuration](/config/mise) guide.
|
||||
|
||||
## Previous and default versions
|
||||
|
||||
@@ -40,4 +41,13 @@ Passing in a previous version will only be used in place of the default. If a
|
||||
more specific version of a package is requested (e.g. through a package.json
|
||||
engines field or env var), then we will always use that.
|
||||
|
||||
This is done on Railway automatically.
|
||||
This is done on Railway automatically.
|
||||
|
||||
## Semver constraints from app manifests
|
||||
|
||||
Manifest constraints (for example `engines.pnpm`) are not resolved with
|
||||
npm-style semver. Caret (`^`) and range (`>=`, `<`) notation are simplified to
|
||||
the major version only (`^10.34.0` → `10`) before Mise picks a release. This
|
||||
is done because Mise does not support compound version constraints right now.
|
||||
Use an exact version or `mise.toml` to pin. See also
|
||||
[`minimum_release_age`](/config/mise#minimum_release_age).
|
||||
@@ -28,7 +28,7 @@ Railpack sets the following mise settings by default in the generated
|
||||
| `paranoid` | `true` | Enforces HTTPS and stricter security validation |
|
||||
| `trusted_config_paths` | `["/app"]` | Trusts app config files to avoid warnings during build |
|
||||
| `idiomatic_version_file_enable_tools` | *(language list)* | Auto-reads version files like `.node-version`, `.python-version`, etc. |
|
||||
| `install_before` | `"14d"` | Only resolves tool versions released more than 14 days ago, avoiding newly-released versions that may be broken |
|
||||
| `minimum_release_age` | `"14d"` | Mise will omit language, package manager, etc versions released in the last two weeks. You can override this settings in your application's mise configuration. |
|
||||
| `node.verify` | `false` | Skips asset signature verification for Node, since recently released versions may not yet have a public key |
|
||||
|
||||
## Customization
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function getPnpmVersion() {
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
return /\bpnpm\/([^\s]+)/.exec(ua)?.[1] ?? null;
|
||||
}
|
||||
|
||||
console.log(`Node.js version: ${process.version}`);
|
||||
|
||||
const pnpmVersionFromUA = getPnpmVersion();
|
||||
if (pnpmVersionFromUA) {
|
||||
console.log(`PNPM version (from user agent): v${pnpmVersionFromUA}`);
|
||||
} else {
|
||||
console.log('PNPM version (from user agent): not detected');
|
||||
}
|
||||
|
||||
try {
|
||||
const pnpmVersion = execSync('pnpm --version', { encoding: 'utf8' }).trim();
|
||||
console.log(`PNPM version (from CLI): v${pnpmVersion}`);
|
||||
} catch (error) {
|
||||
console.log('PNPM version (from CLI): not available');
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// pnpm reads package.json5 natively — https://pnpm.io/package_json
|
||||
"name": "node-pnpm-json5",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Example exercising pnpm's package.json5 manifest support",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node main.js",
|
||||
},
|
||||
"engines": {
|
||||
"node": "26.1.0",
|
||||
"pnpm": "^10.33.0",
|
||||
},
|
||||
}
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"expectedOutput": [
|
||||
"Node.js version: v26.1.0",
|
||||
"PNPM version (from user agent): v10.",
|
||||
"PNPM version (from CLI): v10."
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,28 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { greet as greetA } from "pkg-a";
|
||||
import { greet as greetB } from "pkg-b";
|
||||
|
||||
function getPnpmVersion() {
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
return /\bpnpm\/([^\s]+)/.exec(ua)?.[1] ?? null;
|
||||
}
|
||||
|
||||
console.log("Node v:", process.version);
|
||||
|
||||
const pnpmVersionFromUA = getPnpmVersion();
|
||||
if (pnpmVersionFromUA) {
|
||||
console.log(`PNPM version (from user agent): v${pnpmVersionFromUA}`);
|
||||
} else {
|
||||
console.log("PNPM version (from user agent): not detected");
|
||||
}
|
||||
|
||||
try {
|
||||
const pnpmVersion = execSync("pnpm --version", { encoding: "utf8" }).trim();
|
||||
console.log(`PNPM version (from CLI): v${pnpmVersion}`);
|
||||
} catch {
|
||||
console.log("PNPM version (from CLI): not available");
|
||||
}
|
||||
|
||||
console.log("Hello from pnpm workspaces");
|
||||
|
||||
console.log(greetA());
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
{
|
||||
"expectedOutput": [
|
||||
"Node v: v22.2.0",
|
||||
"PNPM version (from user agent): v9.",
|
||||
"PNPM version (from CLI): v9.",
|
||||
"Hello from pnpm workspaces",
|
||||
"pkg-a loaded",
|
||||
"pkg-b loaded",
|
||||
|
||||
Reference in New Issue
Block a user