I have the following project I am using Pulumi to manage my various cloud run jobs. I am trying to set up all my package management to use a tool like mise
for each of development. The problem is each of the cloud run jobs has their own package.json file and there is no easy way to install all of the node_modules recursively. I have looked at tools like yarn workspaces, or pnpm (with recursive install), but neither works. When I use these tools pulumi up
fails with a syntax error. I can only run pulumi up
on each job after running npm i
in each child directory which is cumbersome.
├── README.md
├── infra
│ ├── job1
│ │ ├── Pulumi.dev.yaml
│ │ ├── Pulumi.prod.yaml
│ │ ├── Pulumi.qa.yaml
│ │ ├── Pulumi.yaml
│ │ ├── index.ts
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── job2
│ │ ├── Pulumi.dev.yaml
│ │ ├── Pulumi.prod.yaml
│ │ ├── Pulumi.qa.yaml
│ │ ├── Pulumi.yaml
│ │ ├── index.ts
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── job3
│ ├── Pulumi.dev.yaml
│ ├── Pulumi.prod.yaml
│ ├── Pulumi.qa.yaml
│ ├── Pulumi.yaml
│ ├── index.ts
│ ├── package-lock.json
│ ├── package.json
│ └── tsconfig.json
├── justfile
├── lib
│ └── job5
│ ├── cloudrun.ts
│ ├── package-lock.json
│ ├── package.json
│ └── tsconfig.json
├── mise.toml
Is there a way in which I can call npm i
recursively from the main project directory? Is it possible to share the package.json files in some way they all essentially are the same:
{
"name": "job1",
"main": "index.ts",
"version": "1.0.0",
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@eslint/migrate-config": "1.3.5",
"@types/node": "^18.19.69",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"eslint": "^9.17.0",
"typescript": "^5.7.2"
},
"dependencies": {
"@lib/cloudrun": "file:../../lib/job5",
"@pulumi/docker": "^4.5.8",
"@pulumi/gcp": "^7.38.0",
"@pulumi/pulumi": "^3.144.1",
"@pulumi/random": "^4.16.8",
"natural-cron": "^1.0.2"
}
}
What is the best practice here? I am open to any and all solutions.
Try implementing the solution below and see if it helps:
Monorepo Package Configuration
{
"name": "pulumi-infrastructure",
"private": true,
"workspaces": [
"infra/*",
"lib/*"
],
"scripts": {
"install-all": "npm install && npm run install:jobs",
"install:jobs": "node scripts/install-jobs.js",
"pulumi:up-all": "node scripts/pulumi-up-all.js"
},
"devDependencies": {
"@types/node": "^18.19.69",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"eslint": "^9.17.0",
"typescript": "^5.7.2"
}
}
NPM Install Script
// scripts/install-jobs.js
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
// Directories to process
const directories = ['infra', 'lib'];
function installDependencies(dir) {
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
const packageJsonPath = path.join(fullPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
console.log(`Installing dependencies in ${fullPath}`);
try {
execSync('npm install', {
cwd: fullPath,
stdio: 'inherit'
});
} catch (error) {
console.error(`Error installing dependencies in ${fullPath}:`, error);
process.exit(1);
}
}
}
});
}
// Process each directory
directories.forEach(dir => {
console.log(`Processing directory: ${dir}`);
installDependencies(dir);
});
Pulumi Up Script
// scripts/pulumi-up-all.js
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
// Get the Pulumi stack from command line or default to 'dev'
const stack = process.argv[2] || 'dev';
function runPulumiUp(dir) {
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
const pulumiYamlPath = path.join(fullPath, 'Pulumi.yaml');
if (fs.existsSync(pulumiYamlPath)) {
console.log(`Running pulumi up in ${fullPath}`);
try {
execSync(`pulumi up --stack ${stack} --yes`, {
cwd: fullPath,
stdio: 'inherit'
});
} catch (error) {
console.error(`Error running pulumi up in ${fullPath}:`, error);
process.exit(1);
}
}
}
});
}
// Process infra directory
console.log('Processing infrastructure jobs...');
runPulumiUp('infra');
Here’s how to implement it:
Create a root package.json
at the project root level that uses workspaces and shared dependencies.
Move shared devDependencies to the root package.json
and keep only project-specific dependencies in the individual job's package.json files.
Create installation and deployment scripts to handle the recursive operations (follow the steps below).
First, simplify your job-specific package.json
files to only include necessary dependencies:
{
"name": "job1",
"main": "index.ts",
"version": "1.0.0",
"dependencies": {
"@lib/cloudrun": "file:../../lib/job5",
"@pulumi/docker": "^4.5.8",
"@pulumi/gcp": "^7.38.0",
"@pulumi/pulumi": "^3.144.1",
"@pulumi/random": "^4.16.8",
"natural-cron": "^1.0.2"
}
}
Create the scripts directory and add the installation and Pulumi scripts I provided above.
You can now use the following commands:
# Install all dependencies recursively
npm run install-all
# Run Pulumi up for all jobs
npm run pulumi:up-all dev # or qa/prod for other environments
Here’s some additional recommendations:
Add a .npmrc
file at the root to ensure consistent package management:
save-exact=true
legacy-peer-deps=true
workspaces=true
Consider adding these scripts to your justfile
for easier execution:
install:
npm run install-all
deploy ENV:
npm run pulumi:up-all {{ENV}}
With this approach, shared devDependencies are managed at root level. Each job can still maintain its own specific dependencies, and the scripts provide a consistent way to install dependencies and run Pulumi commands. You can still use mise
for version management at the root level.